├── .github ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── swiftpm │ │ └── Package.resolved │ │ └── xcschemes │ │ ├── FoundationExtensions.xcscheme │ │ ├── FoundationExtensionsMacros.xcscheme │ │ ├── FoundationExtensionsMacrosPluginTests.xcscheme │ │ ├── FoundationExtensionsMacrosTests.xcscheme │ │ ├── FoundationExtensionsTests.xcscheme │ │ └── swift-foundation-extensions-Package.xcscheme └── workflows │ └── ci.yml ├── .gitignore ├── .spi.yml ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ ├── FoundationExtensions.xcscheme │ ├── FoundationExtensionsMacros.xcscheme │ ├── FoundationExtensionsMacrosPluginTests.xcscheme │ ├── FoundationExtensionsMacrosTests.xcscheme │ ├── FoundationExtensionsTests.xcscheme │ └── swift-foundation-extensions-Package.xcscheme ├── LICENCE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── FoundationExtensions │ ├── Exports.swift │ ├── Extensions │ │ ├── Bundle+.swift │ │ ├── Codable+.swift │ │ ├── Collection+.swift │ │ ├── DispatchTime+.swift │ │ ├── FloatingPoint+.swift │ │ ├── NSAttributedString+.swift │ │ ├── NSLocking+.swift │ │ ├── Optional+.swift │ │ ├── Range+.swift │ │ ├── Result+.swift │ │ └── String+.swift │ └── General │ │ ├── AssociatingObject │ │ ├── AssociatingObject.swift │ │ └── objc_AssociationPolicy+.swift │ │ ├── Box&Reference │ │ ├── Box.swift │ │ └── Reference.swift │ │ ├── Castable.swift │ │ ├── Equated │ │ ├── Equated+Comparator.swift │ │ └── Equated.swift │ │ ├── Indirect.swift │ │ ├── PropertyProxy.swift │ │ ├── Resettable │ │ ├── Resettable+CollectionProxy.swift │ │ └── Resettable.swift │ │ ├── RuntimeWarnings.swift │ │ ├── Swizzling │ │ └── NSObject+Swizzling.swift │ │ ├── USID.swift │ │ └── UnwrappingError.swift ├── FoundationExtensionsMacros │ ├── AssociatedObject.swift │ └── Exports.swift └── FoundationExtensionsMacrosPlugin │ ├── AssociatedObjectMacro │ └── AssociatedObjectMacro.swift │ ├── Helpers │ ├── Diagnostics+.swift │ └── Operators.swift │ └── Plugin.swift └── Tests ├── FoundationExtensionsMacrosPluginTests └── AssociatedObjectTests.swift ├── FoundationExtensionsMacrosTests └── AssociatedObjectTests.swift └── FoundationExtensionsTests ├── AssociatingObjectTests.swift ├── CodingKeysTests.swift ├── EquatedTests.swift ├── IndirectTests.swift ├── ObjectProxyTests.swift ├── ReferenceTests.swift ├── ResettableTests.swift └── SwizzlingTests.swift /.github/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-custom-dump", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 7 | "state" : { 8 | "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", 9 | "version" : "1.1.2" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-declarative-configuration", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/capturecontext/swift-declarative-configuration.git", 16 | "state" : { 17 | "revision" : "6d191cb2414de9f55b972eb20592bcf22591fa4c", 18 | "version" : "0.3.2" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-macro-testing", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-macro-testing.git", 25 | "state" : { 26 | "revision" : "10dcef36314ddfea6f60442169b0b320204cbd35", 27 | "version" : "0.2.2" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-macro-toolkit", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/stackotter/swift-macro-toolkit.git", 34 | "state" : { 35 | "revision" : "106daeb38eb3f52b1540aed981fc63fa22274576", 36 | "version" : "0.3.1" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-snapshot-testing", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 43 | "state" : { 44 | "revision" : "59b663f68e69f27a87b45de48cb63264b8194605", 45 | "version" : "1.15.1" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-syntax", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-syntax.git", 52 | "state" : { 53 | "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", 54 | "version" : "509.0.0" 55 | } 56 | }, 57 | { 58 | "identity" : "xctest-dynamic-overlay", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 61 | "state" : { 62 | "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", 63 | "version" : "1.0.2" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /.github/package.xcworkspace/xcshareddata/xcschemes/FoundationExtensions.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.github/package.xcworkspace/xcshareddata/xcschemes/FoundationExtensionsMacros.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.github/package.xcworkspace/xcshareddata/xcschemes/FoundationExtensionsMacrosPluginTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.github/package.xcworkspace/xcshareddata/xcschemes/FoundationExtensionsMacrosTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.github/package.xcworkspace/xcshareddata/xcschemes/FoundationExtensionsTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.github/package.xcworkspace/xcshareddata/xcschemes/swift-foundation-extensions-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 71 | 77 | 78 | 79 | 80 | 81 | 87 | 88 | 90 | 96 | 97 | 98 | 100 | 106 | 107 | 108 | 110 | 116 | 117 | 118 | 119 | 120 | 130 | 131 | 137 | 138 | 144 | 145 | 146 | 147 | 149 | 150 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | concurrency: 12 | group: ci-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | library-swift-latest: 17 | name: Library 18 | if: | 19 | !contains(github.event.head_commit.message, '[ci skip]') && 20 | !contains(github.event.head_commit.message, '[ci skip test]') && 21 | !contains(github.event.head_commit.message, '[ci skip library-swift-latest]') 22 | runs-on: macos-13 23 | timeout-minutes: 30 24 | strategy: 25 | matrix: 26 | config: 27 | - debug 28 | - release 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Select Xcode 15.2 32 | run: sudo xcode-select -s /Applications/Xcode_15.2.app 33 | - name: Run test 34 | run: make test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [FoundationExtensions] 5 | swift_version: 5.9 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/FoundationExtensions.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/FoundationExtensionsMacros.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/FoundationExtensionsMacrosPluginTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/FoundationExtensionsMacrosTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/FoundationExtensionsTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/swift-foundation-extensions-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 71 | 77 | 78 | 79 | 80 | 81 | 87 | 88 | 90 | 96 | 97 | 98 | 100 | 106 | 107 | 108 | 110 | 116 | 117 | 118 | 119 | 120 | 130 | 131 | 137 | 138 | 144 | 145 | 146 | 147 | 149 | 150 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CaptureContext 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 | CONFIG = debug 2 | PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS 17.2,iPhone \d\+ Pro [^M]) 3 | PLATFORM_MACOS = macOS 4 | PLATFORM_MAC_CATALYST = macOS,variant=Mac Catalyst 5 | PLATFORM_TVOS = tvOS Simulator,id=$(call udid_for,tvOS 17,TV) 6 | PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,watchOS 10,Watch) 7 | 8 | default: test-all 9 | 10 | test-all: 11 | $(MAKE) test 12 | $(MAKE) test-docs 13 | 14 | test: 15 | $(MAKE) CONFIG=debug test-library 16 | $(MAKE) CONFIG=debug test-library-macros 17 | $(MAKE) test-macros 18 | 19 | test-library: 20 | for platform in "$(PLATFORM_IOS)" "$(PLATFORM_MACOS)" "$(PLATFORM_MAC_CATALYST)" "$(PLATFORM_TVOS)" "$(PLATFORM_WATCHOS)"; do \ 21 | echo "\nTesting library on $$platform\n" && \ 22 | (xcodebuild test \ 23 | -skipMacroValidation \ 24 | -configuration $(CONFIG) \ 25 | -workspace .github/package.xcworkspace \ 26 | -scheme FoundationExtensionsTests \ 27 | -destination platform="$$platform" | xcpretty && exit 0 \ 28 | ) \ 29 | || exit 1; \ 30 | done; 31 | 32 | test-library-macros: 33 | for platform in "$(PLATFORM_IOS)" "$(PLATFORM_MACOS)" "$(PLATFORM_MAC_CATALYST)" "$(PLATFORM_TVOS)" "$(PLATFORM_WATCHOS)"; do \ 34 | echo "\nTesting library-macros on $$platform\n" && \ 35 | (xcodebuild test \ 36 | -skipMacroValidation \ 37 | -configuration $(CONFIG) \ 38 | -workspace .github/package.xcworkspace \ 39 | -scheme FoundationExtensionsMacrosTests \ 40 | -destination platform="$$platform" | xcpretty && exit 0 \ 41 | ) \ 42 | || exit 1; \ 43 | done; 44 | 45 | test-macros: 46 | echo "\nTesting macros\n" && \ 47 | (xcodebuild test \ 48 | -skipMacroValidation \ 49 | -configuration $(CONFIG) \ 50 | -workspace .github/package.xcworkspace \ 51 | -scheme FoundationExtensionsMacrosPluginTests \ 52 | -destination platform=macOS | xcpretty && exit 0 \ 53 | ) \ 54 | || exit 1; 55 | 56 | DOC_WARNINGS = $(shell xcodebuild clean docbuild \ 57 | -scheme FoundationExtensions \ 58 | -destination platform="$(PLATFORM_IOS)" \ 59 | -quiet \ 60 | 2>&1 \ 61 | | grep "couldn't be resolved to known documentation" \ 62 | | sed 's|$(PWD)|.|g' \ 63 | | tr '\n' '\1') 64 | test-docs: 65 | @test "$(DOC_WARNINGS)" = "" \ 66 | || (echo "xcodebuild docbuild failed:\n\n$(DOC_WARNINGS)" | tr '\1' '\n' \ 67 | && exit 1) 68 | 69 | define udid_for 70 | $(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }') 71 | endef 72 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-custom-dump", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 7 | "state" : { 8 | "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", 9 | "version" : "1.1.2" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-declarative-configuration", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/capturecontext/swift-declarative-configuration.git", 16 | "state" : { 17 | "revision" : "6d191cb2414de9f55b972eb20592bcf22591fa4c", 18 | "version" : "0.3.2" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-macro-testing", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-macro-testing.git", 25 | "state" : { 26 | "revision" : "10dcef36314ddfea6f60442169b0b320204cbd35", 27 | "version" : "0.2.2" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-macro-toolkit", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/stackotter/swift-macro-toolkit.git", 34 | "state" : { 35 | "revision" : "106daeb38eb3f52b1540aed981fc63fa22274576", 36 | "version" : "0.3.1" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-snapshot-testing", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 43 | "state" : { 44 | "revision" : "59b663f68e69f27a87b45de48cb63264b8194605", 45 | "version" : "1.15.1" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-syntax", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-syntax.git", 52 | "state" : { 53 | "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", 54 | "version" : "509.0.0" 55 | } 56 | }, 57 | { 58 | "identity" : "xctest-dynamic-overlay", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 61 | "state" : { 62 | "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", 63 | "version" : "1.0.2" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | import CompilerPluginSupport 5 | 6 | let package = Package( 7 | name: "swift-foundation-extensions", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .macCatalyst(.v13), 11 | .iOS(.v13), 12 | .tvOS(.v13), 13 | .watchOS(.v6) 14 | ], 15 | products: [ 16 | .library( 17 | name: "FoundationExtensions", 18 | targets: ["FoundationExtensions"] 19 | ), 20 | .library( 21 | name: "FoundationExtensionsMacros", 22 | targets: ["FoundationExtensionsMacros"] 23 | ), 24 | ], 25 | dependencies: [ 26 | .package( 27 | url: "https://github.com/capturecontext/swift-declarative-configuration.git", 28 | .upToNextMinor(from: "0.3.0") 29 | ), 30 | .package( 31 | url: "https://github.com/pointfreeco/swift-custom-dump", 32 | .upToNextMajor(from: "1.0.0") 33 | ), 34 | .package( 35 | url: "https://github.com/stackotter/swift-macro-toolkit.git", 36 | .upToNextMinor(from: "0.3.0") 37 | ), 38 | .package( 39 | url: "https://github.com/pointfreeco/swift-macro-testing.git", 40 | .upToNextMinor(from: "0.2.2") 41 | ) 42 | ], 43 | targets: [ 44 | .target( 45 | name: "FoundationExtensions", 46 | dependencies: [ 47 | .product( 48 | name: "FunctionalKeyPath", 49 | package: "swift-declarative-configuration" 50 | ), 51 | .product( 52 | name: "CustomDump", 53 | package: "swift-custom-dump" 54 | ), 55 | ] 56 | ), 57 | .target( 58 | name: "FoundationExtensionsMacros", 59 | dependencies: [ 60 | .target(name: "FoundationExtensions"), 61 | .target(name: "FoundationExtensionsMacrosPlugin"), 62 | ] 63 | ), 64 | .macro( 65 | name: "FoundationExtensionsMacrosPlugin", 66 | dependencies: [ 67 | .product( 68 | name: "MacroToolkit", 69 | package: "swift-macro-toolkit" 70 | ) 71 | ] 72 | ), 73 | .testTarget( 74 | name: "FoundationExtensionsTests", 75 | dependencies: [ 76 | .target(name: "FoundationExtensions"), 77 | ] 78 | ), 79 | .testTarget( 80 | name: "FoundationExtensionsMacrosPluginTests", 81 | dependencies: [ 82 | .target(name: "FoundationExtensionsMacrosPlugin"), 83 | .product(name: "MacroTesting", package: "swift-macro-testing"), 84 | ] 85 | ), 86 | .testTarget( 87 | name: "FoundationExtensionsMacrosTests", 88 | dependencies: [ 89 | .target(name: "FoundationExtensionsMacros"), 90 | ] 91 | ), 92 | ] 93 | ) 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-foundation-extensions 2 | 3 | [![CI](https://github.com/CaptureContext/swift-foundation-extensions/actions/workflows/ci.yml/badge.svg)](https://github.com/CaptureContext/swift-foundation-extensions/actions/workflows/ci.yml) [![SwiftPM 5.9](https://img.shields.io/badge/swiftpm-5.9-ED523F.svg?style=flat)](https://swift.org/download/) ![Platforms](https://img.shields.io/badge/Platforms-iOS_13_|_macOS_10.15_|_tvOS_14_|_watchOS_7-ED523F.svg?style=flat) [![@capture_context](https://img.shields.io/badge/contact-@capturecontext-1DA1F2.svg?style=flat&logo=twitter)](https://twitter.com/capture_context) 4 | 5 | Standard extensions for Foundation framework 6 | 7 | - [Documentation](https://swiftpackageindex.com/CaptureContext/swift-foundation-extensions/0.5.0/documentation/foundationextensions) 8 | - [Contents](#contents) 9 | - [Coding](#coding) 10 | - [NSLocking](#nslocking) 11 | - [Optional](#optional) 12 | - [Undo/Redo management](#undoredo-management) 13 | - [Indirect](#indirect) 14 | - [Property Proxy](#property-proxy) 15 | - [Object Association](#object-Association) 16 | - [Swizzling](#swizzling) 17 | - [Installation](#installation) 18 | - [Basic](#basic) 19 | - [Recommended](#recommended) 20 | - [Licence](#licence) 21 | 22 | ## Contents 23 | 24 | ### Coding 25 | 26 | - RawCodingKey allows you to create CodingKeys from literals 27 | 28 | - Extensions for encoder and decoder allow you to create an object with a contextual container 29 | 30 | - Extensions for coding containers automatically infer type from context 31 | 32 | ```swift 33 | init(from decoder: Decoder) throws { 34 | self = try container.decode(RawCodingKey.self) { container in 35 | return .init( 36 | someProperty1: container.decode("someProperty1"), 37 | someProperty2: container.decode("some_property_2") 38 | ) 39 | } 40 | } 41 | 42 | func encode(to encoder: encoder) throws { 43 | try encoder.encode(RawCodingKey.self) { container in 44 | try container.encode(someProperty1, forKey: "someProperty1") 45 | try container.encode(someProperty2, forKey: "some_property_2") 46 | } 47 | } 48 | ``` 49 | 50 | ### NSLocking 51 | 52 | - `store(_:in:)` - stores value in some variable in locked context 53 | - `mutate(_:with:)` - passes given object to locked context 54 | - `assign(_:to:on:)` - stores value in object property in locked context 55 | - `execute(_:)` - provides new locked context 56 | 57 | ### Optional 58 | 59 | - `orThrow(_:)` - unwraps an optional or throws specified error 60 | - `isNil` / `isNotNil` / `isNilOrEmpty` 61 | - `or()` - coalesing alias 62 | - `unwrap()` - returns unwrapping Result 63 | - `assign(to:on:)` - assigns wrapped value to a specified target property by the keyPath 64 | - `ifLetAssign(to:on:)` - assigns wrapped value to a specified target property by the keyPath if an optional was not nil 65 | 66 | ### Undo/Redo management 67 | 68 | ```swift 69 | struct State { 70 | var value: Int = 0 71 | } 72 | 73 | @Resettable 74 | let state = State() 75 | state.value = 1 // value == 1 76 | state.value *= 10 // value == 10 77 | state.undo() // value == 1 78 | state.value += 1 // value == 2 79 | state.undo() // value == 1 80 | state.redo() // value == 2 81 | ``` 82 | 83 | ### Indirect 84 | 85 | CoW container, which allows you to recursively include single instances of value types 86 | 87 | ```swift 88 | struct ListNode { 89 | var value: Value 90 | 91 | @Indirect 92 | var next: ListNode? 93 | } 94 | ``` 95 | 96 | 97 | 98 | ### PropertyProxy 99 | 100 | ```swift 101 | class MyView: UIView { 102 | private let label: UILabel 103 | 104 | @PropertyProxy(\MyView.label.text) 105 | var text: String? 106 | } 107 | 108 | let view: MyView = .init() 109 | view.label.text // ❌ 110 | view.text = "Hello, World!" 111 | ``` 112 | 113 | ### Object Association 114 | 115 | Basic helpers for object association are available in a base package 116 | 117 | ```swift 118 | extension UIViewController { 119 | var someStoredProperty: Int { 120 | get { getAssociatedObject(forKey: #function).or(0) } 121 | set { setAssociatedObject(newValue, forKey: #function) } 122 | } 123 | } 124 | 125 | let value: Bool = getAssociatedObject(forKey: "value", from: object) 126 | ``` 127 | 128 | But the full power of associated objects is provided by `FoundationExtensionsMacros` target 129 | 130 | > By default `@AssociatedObject` macro uses `.retain(.nonatomic)` for classes and `.copy(.nonatomic)` `objc_AssociationPolicy` for structs. 131 | 132 | ```swift 133 | import FoundationExtensionsMacros 134 | 135 | extension SomeClass { 136 | @AssociatedObject 137 | var storedVariableInExtension: Int = 0 138 | 139 | @AssociatedObject(readonly: true) 140 | var storedVariableInExtension: SomeObject = .init() 141 | 142 | @AssociatedObject 143 | var optionalValue: Int? 144 | 145 | @AssociatedObject 146 | var object: Int? 147 | 148 | @AssociatedObject(threadSafety: .atomic) 149 | var threadSafeValue: Int? 150 | 151 | @AssociatedObject(threadSafety: .atomic) 152 | var threadSafeObject: Object? 153 | 154 | @AssociatedObject(policy: .assign) 155 | var customPolicyValue: Int? 156 | 157 | @AssociatedObject(policy: .retain(.atomic)) 158 | var customPolicyThreadSafeObject: Object? 159 | } 160 | ``` 161 | 162 | > Macros require swift-syntax compilation, so it will affect cold compilation time 163 | 164 | ### Swizzling 165 | 166 | This package also provides some sugar for objc method swizzling 167 | 168 | ```swift 169 | extension UIViewController { 170 | // Runs once in app lifetime 171 | // Repeated calls do nothing 172 | private static let swizzle: Void = { 173 | // This example is not really representative since these methods 174 | // can be simply globally overriden, but it's just an example 175 | // for the readme and you can find live example at 176 | // https://github.com/capturecontext/combine-cocoa-navigation 177 | 178 | objc_exchangeImplementations( 179 | #selector(viewWillAppear) 180 | #selector(__swizzledViewWillAppear) 181 | ) 182 | 183 | objc_exchangeImplementations( 184 | #selector(viewDidAppear) 185 | #selector(__swizzledViewDidAppear) 186 | ) 187 | }() 188 | 189 | @objc dynamic 190 | private func __swizzledViewWillAppear(_ animated: Bool) { 191 | __swizzledViewWillAppear(animated) // calls original method 192 | print(type(of: self), ObjectIdentifier(self), "will appear") 193 | } 194 | 195 | @objc dynamic 196 | private func __swizzledViewDidAppear(_ animated: Bool) { 197 | __swizzledViewDidAppear(animated) // calls original method 198 | print(type(of: self), ObjectIdentifier(self), "did appear") 199 | } 200 | } 201 | ``` 202 | 203 | ## Installation 204 | 205 | ### Basic 206 | 207 | You can add FoundationExtensions to an Xcode project by adding it as a package dependency. 208 | 209 | 1. From the **File** menu, select **Swift Packages › Add Package Dependency…** 210 | 2. Enter [`"https://github.com/capturecontext/swift-foundation-extensions.git"`](https://github.com/capturecontext/swift-foundation-extensions.git) into the package repository URL text field 211 | 3. Choose products you need to link them to your project. 212 | 213 | ### Recommended 214 | 215 | If you use SwiftPM for your project, you can add StandardExtensions to your package file. 216 | 217 | ```swift 218 | .package( 219 | url: "https://github.com/capturecontext/swift-foundation-extensions.git", 220 | .upToNextMinor(from: "0.5.0") 221 | ) 222 | ``` 223 | 224 | Do not forget about target dependencies: 225 | 226 | ```swift 227 | .product( 228 | name: "FoundationExtensions", 229 | package: "swift-foundation-extensions" 230 | ) 231 | ``` 232 | 233 | ```swift 234 | .product( 235 | name: "FoundationExtensionsMacros", 236 | package: "swift-foundation-extensions" 237 | ) 238 | ``` 239 | 240 | 241 | 242 | ## License 243 | 244 | This library is released under the MIT license. See [LICENCE](LICENCE) for details. 245 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import Foundation 2 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/Bundle+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Bundle { 4 | @inlinable 5 | public var appVersionNumber: String? { infoDictionary?["CFBundleShortVersionString"] as? String } 6 | 7 | @inlinable 8 | public var buildVersionNumber: String? { infoDictionary?["CFBundleVersion"] as? String } 9 | 10 | @inlinable 11 | public var teamIdentifierPrefix: String? { infoDictionary?["TeamIdentifierPrefix"] as? String } 12 | 13 | /// KeyPrefix that can be `.` or empty if BundleID is inaccessable 14 | @inlinable 15 | public var keyPrefix: String { 16 | return bundleIdentifier.map { $0.appending(".") }.or("") 17 | } 18 | 19 | /// Creates a key prefixed by bundle.keyPrefix 20 | @inlinable 21 | public func makeKey(_ key: String) -> String { 22 | return keyPrefix.appending(key) 23 | } 24 | 25 | /// Creates appGroupID with a specified teamIdentifier prefixed by bundle.teamIdentifierPrefix 26 | /// 27 | /// Returns nil if teamIdentifierPrefix is inaccessable 28 | @inlinable 29 | public func appGroupID(for teamIdentifier: String) -> String? { 30 | return teamIdentifierPrefix.map { "\($0)\(teamIdentifier)" } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/Codable+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum RawCodingKey: CodingKey, ExpressibleByStringLiteral, ExpressibleByIntegerLiteral { 4 | case key(String) 5 | case index(Int) 6 | 7 | @inlinable 8 | public init(stringLiteral value: String) { 9 | self.init(stringValue: value) 10 | } 11 | 12 | @inlinable 13 | public init(stringValue: String) { 14 | self = .key(stringValue) 15 | } 16 | 17 | @inlinable 18 | public init(integerLiteral value: Int) { 19 | self.init(intValue: value) 20 | } 21 | 22 | @inlinable 23 | public init(intValue: Int) { 24 | self = .index(intValue) 25 | } 26 | 27 | @inlinable 28 | public var stringValue: String { 29 | switch self { 30 | case let .key(value): 31 | return value 32 | case let .index(value): 33 | return value.description 34 | } 35 | } 36 | 37 | @inlinable 38 | public var intValue: Int? { 39 | switch self { 40 | case let .index(value): 41 | return value 42 | default: 43 | return nil 44 | } 45 | } 46 | } 47 | 48 | extension Decoder { 49 | @inlinable 50 | public func decode( 51 | _ decode: (KeyedDecodingContainer) throws -> T 52 | ) throws -> T { 53 | return try self.decode(RawCodingKey.self, decode) 54 | } 55 | 56 | @inlinable 57 | public func decode( 58 | _ codingKeys: CodingKeys.Type, 59 | _ decode: (KeyedDecodingContainer) throws -> T 60 | ) throws -> T { 61 | let container = try container(keyedBy: codingKeys) 62 | return try decode(container) 63 | } 64 | } 65 | 66 | extension Encoder { 67 | @inlinable 68 | public func encode( 69 | _ encode: (inout KeyedEncodingContainer) throws -> T 70 | ) throws -> T { 71 | return try self.encode(RawCodingKey.self, encode) 72 | } 73 | 74 | @inlinable 75 | public func encode( 76 | _ codingKeys: CodingKeys.Type, 77 | _ encode: (inout KeyedEncodingContainer) throws -> T 78 | ) throws -> T { 79 | var container = container(keyedBy: codingKeys) 80 | return try encode(&container) 81 | } 82 | } 83 | 84 | extension KeyedDecodingContainer { 85 | @inlinable 86 | public func decode( 87 | _ key: K 88 | ) throws -> T { 89 | try decode(T.self, forKey: key) 90 | } 91 | 92 | @inlinable 93 | public func decodeIfPresent( 94 | _ key: K 95 | ) throws -> T? { 96 | try decodeIfPresent(T.self, forKey: key) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/Collection+.swift: -------------------------------------------------------------------------------- 1 | extension Collection { 2 | /// - Complexity: *O(1)* 3 | @inlinable 4 | public var isNotEmpty: Bool { !self.isEmpty } 5 | } 6 | 7 | extension Array { 8 | /// - Complexity: *O(n)* 9 | @inlinable 10 | public mutating func bringFront(elementsSatisfying predicate: (Element) -> Bool) { 11 | let leftHalf = self.filter { predicate($0) } 12 | let rightHalf = self.filter { !predicate($0) } 13 | self = leftHalf + rightHalf 14 | } 15 | } 16 | 17 | extension MutableCollection { 18 | /// - Complexity: *O(1)* 19 | @inlinable 20 | public subscript(safe index: Index?) -> Element? { 21 | get { 22 | guard let index = index else { return nil } 23 | return self[safe: index] 24 | } 25 | set { 26 | guard let index = index else { return } 27 | self[safe: index] = newValue 28 | } 29 | } 30 | 31 | /// - Complexity: *O(1)* 32 | @inlinable 33 | public subscript(safe index: Index) -> Element? { 34 | get { 35 | guard indices.contains(index) 36 | else { return nil } 37 | return self[index] 38 | } 39 | set { 40 | guard 41 | indices.contains(index), 42 | let value = newValue 43 | else { return } 44 | return self[index] = value 45 | } 46 | } 47 | } 48 | 49 | extension Collection { 50 | /// - Complexity: *O(1)* 51 | @inlinable 52 | public subscript(safe index: Index?) -> Element? { 53 | get { 54 | guard let index = index else { return nil } 55 | return self[safe: index] 56 | } 57 | } 58 | 59 | /// - Complexity: *O(1)* 60 | @inlinable 61 | public subscript(safe index: Index) -> Element? { 62 | get { 63 | guard indices.contains(index) 64 | else { return nil } 65 | return self[index] 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/DispatchTime+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension DispatchTimeInterval { 4 | /// Creates DispatchTimeInterval.nanoseconds for the specified interval in seconds 5 | @inlinable 6 | public static func interval(_ value: TimeInterval) -> DispatchTimeInterval { 7 | return .nanoseconds(Int(value * pow(10, 9))) 8 | } 9 | } 10 | 11 | extension DispatchTime: ExpressibleByFloatLiteral { 12 | /// Creates DispatchTime for the specified interval in seconds from `.now()` 13 | @inlinable 14 | public static func interval(_ interval: TimeInterval) -> DispatchTime { 15 | return .now() + .interval(interval) 16 | } 17 | 18 | @inlinable 19 | public init(floatLiteral value: TimeInterval) { 20 | self = .interval(value) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/FloatingPoint+.swift: -------------------------------------------------------------------------------- 1 | extension FloatingPoint { 2 | @inlinable 3 | public func progress(in total: Self) -> Self { 4 | total != 0 ? self / total : 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/NSAttributedString+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NSMutableAttributedString { 4 | @inlinable 5 | public func addAttribute(_ key: Key, value: Any) { 6 | addAttribute(key, value: value, range: NSRange(0.. Void 15 | ) -> NSAttributedString { 16 | let attributedString = NSMutableAttributedString(attributedString: self) 17 | let range = NSRange(0.. Void 37 | ) -> NSAttributedString { 38 | let attributedString = NSMutableAttributedString(attributedString: self) 39 | let range = NSRange(0..(_ value: T, in object: inout T) { 7 | mutate(&object, with: { $0 = value }) 8 | } 9 | 10 | /// Atomically mutates object with closure 11 | @inlinable 12 | public func mutate(_ object: T, with closure: (T) -> Void) { 13 | execute { closure(object) } 14 | } 15 | 16 | /// Atomically mutates object with closure 17 | @inlinable 18 | public func mutate(_ object: inout T, with closure: (inout T) -> Void) { 19 | execute { closure(&object) } 20 | } 21 | 22 | /// Atomically assigns value to specified object property 23 | @inlinable 24 | public func assign( 25 | _ value: Value, 26 | to keyPath: ReferenceWritableKeyPath, 27 | on object: T 28 | ) { 29 | execute { object[keyPath: keyPath] = value } 30 | } 31 | 32 | /// Atomically assigns value to specified object property 33 | @inlinable 34 | public func assign( 35 | _ value: Value, 36 | to keyPath: WritableKeyPath, 37 | on object: inout T 38 | ) { 39 | execute { object[keyPath: keyPath] = value } 40 | } 41 | 42 | /// Atomically executes the block of code 43 | @discardableResult 44 | @inlinable 45 | public func execute(_ closure: () -> T) -> T { 46 | withLock(closure) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/Optional+.swift: -------------------------------------------------------------------------------- 1 | extension Optional { 2 | /// Unwraps an optional or throws specified error 3 | @inlinable 4 | public func orThrow(_ error: @autoclosure () -> Error) throws -> Wrapped { 5 | switch self { 6 | case .some(let wrapped): 7 | return wrapped 8 | case .none: 9 | throw error() 10 | } 11 | } 12 | 13 | @inlinable 14 | public var isNil: Bool { 15 | switch self { 16 | case .none: return true 17 | case .some: return false 18 | } 19 | } 20 | 21 | @inlinable 22 | public var isNotNil: Bool { !isNil } 23 | 24 | /// Unwraps an optional and returns specified value if the optional was nil 25 | @inlinable 26 | public func or(_ value: @autoclosure () -> Wrapped) -> Wrapped { 27 | self ?? value() 28 | } 29 | 30 | /// Unwraps an optional and returns specified value if the optional was nil 31 | @inlinable 32 | public func or(_ value: @autoclosure () -> Wrapped?) -> Wrapped? { 33 | self ?? value() 34 | } 35 | 36 | /// Unwraps an optional and returns unwrapping result 37 | @inlinable 38 | public func unwrap(function: String = #function, file: String = #filePath, line: Int = #line) 39 | -> Result> 40 | { 41 | switch self { 42 | case .some(let value): 43 | return .success(value) 44 | case .none: 45 | return .failure( 46 | UnwrappingError( 47 | function: function, 48 | file: file, 49 | line: line) 50 | ) 51 | } 52 | } 53 | 54 | /// Assigns wrapped value to a specified target property by the keyPath 55 | @inlinable 56 | public func assign( 57 | to keyPath: ReferenceWritableKeyPath, 58 | on target: T 59 | ) { target[keyPath: keyPath] = self } 60 | 61 | /// Assigns wrapped value to a specified target property by the keyPath if an optional was not nil 62 | @inlinable 63 | public func ifLetAssign( 64 | to keyPath: ReferenceWritableKeyPath, 65 | on target: T 66 | ) { map { target[keyPath: keyPath] = $0 } } 67 | 68 | /// Assigns wrapped value to a specified target property by the keyPath if an optional was not nil 69 | @inlinable 70 | public func ifLetAssign( 71 | to keyPath: ReferenceWritableKeyPath, 72 | on target: T 73 | ) { map { target[keyPath: keyPath] = $0 } } 74 | 75 | /// Assigns wrapped value to a specified target property by the keyPath 76 | @inlinable 77 | public func assign( 78 | to keyPath: WritableKeyPath, 79 | on target: inout T 80 | ) { target[keyPath: keyPath] = self } 81 | 82 | /// Assigns wrapped value to a specified target property by the keyPath if an optional was not nil 83 | @inlinable 84 | public func ifLetAssign( 85 | to keyPath: WritableKeyPath, 86 | on target: inout T 87 | ) { map { target[keyPath: keyPath] = $0 } } 88 | 89 | /// Assigns wrapped value to a specified target property by the keyPath if an optional was not nil 90 | @inlinable 91 | public func ifLetAssign( 92 | to keyPath: WritableKeyPath, 93 | on target: inout T 94 | ) { map { target[keyPath: keyPath] = $0 } } 95 | } 96 | 97 | extension Optional where Wrapped: Collection { 98 | @inlinable 99 | public var isNilOrEmpty: Bool { 100 | map(\.isEmpty).or(true) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/Range+.swift: -------------------------------------------------------------------------------- 1 | extension Range where Bound: BinaryInteger { 2 | @inlinable 3 | public var length: Bound { upperBound - lowerBound } 4 | } 5 | 6 | extension ClosedRange where Bound: Numeric { 7 | @inlinable 8 | public var length: Bound { upperBound - lowerBound } 9 | } 10 | 11 | extension ClosedRange { 12 | @inlinable 13 | public func clamped(_ value: Bound) -> Bound { 14 | if value < lowerBound { 15 | return lowerBound 16 | } else if value > upperBound { 17 | return upperBound 18 | } else { 19 | return value 20 | } 21 | } 22 | } 23 | 24 | extension PartialRangeFrom { 25 | @inlinable 26 | public func clamped(_ value: Bound) -> Bound { 27 | if value < lowerBound { 28 | return lowerBound 29 | } else { 30 | return value 31 | } 32 | } 33 | } 34 | 35 | extension PartialRangeThrough { 36 | @inlinable 37 | public func clamped(_ value: Bound) -> Bound { 38 | if value > upperBound { 39 | return upperBound 40 | } else { 41 | return value 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/Result+.swift: -------------------------------------------------------------------------------- 1 | extension Swift.Result { 2 | @inlinable 3 | public var value: Success? { 4 | switch self { 5 | case let .success(value): return value 6 | default: return nil 7 | } 8 | } 9 | 10 | @inlinable 11 | public var error: Failure? { 12 | switch self { 13 | case let .failure(error): return error 14 | default: return nil 15 | } 16 | } 17 | 18 | @inlinable 19 | public func eraseError() -> Result { 20 | mapError { $0 as Error } 21 | } 22 | 23 | @inlinable 24 | public func replaceError(with success: Success) -> Result { 25 | replaceError { _ in success } 26 | } 27 | 28 | @inlinable 29 | public func replaceError(with closure: (Error) -> Success) -> Result { 30 | switch self { 31 | case .success(let value): 32 | return .success(value) 33 | case .failure(let error): 34 | return .success(closure(error)) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/Extensions/String+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | /// Returns ranges of all occrances of subsring in string 5 | @inlinable 6 | public func ranges( 7 | of substring: String, 8 | options: CompareOptions = [], 9 | locale: Locale? = nil 10 | ) -> [Range] { 11 | var ranges: [Range] = [] 12 | while 13 | let range = range( 14 | of: substring, 15 | options: options, 16 | range: (ranges.last?.upperBound ?? startIndex)..( 7 | _ object: Object?, 8 | forKey key: StaticString, 9 | policy: objc_AssociationPolicy 10 | ) -> Bool 11 | 12 | @inlinable 13 | @discardableResult 14 | func setAssociatedObject( 15 | _ object: Object?, 16 | forKey key: StaticString, 17 | threadSafety: _AssociationPolicyThreadSafety 18 | ) -> Bool 19 | 20 | @inlinable 21 | func getAssociatedObject( 22 | of type: Object.Type, 23 | forKey key: StaticString 24 | ) -> Object? 25 | } 26 | 27 | extension AssociatingObject { 28 | @inlinable 29 | @discardableResult 30 | public func setAssociatedObject( 31 | _ object: Object?, 32 | forKey key: StaticString, 33 | policy: objc_AssociationPolicy = .retain(.nonatomic) 34 | ) -> Bool { 35 | return _setAssociatedObject( 36 | object, 37 | to: self, 38 | forKey: key, 39 | policy: policy 40 | ) 41 | } 42 | 43 | @inlinable 44 | @discardableResult 45 | public func setAssociatedObject( 46 | _ object: Object?, 47 | forKey key: StaticString, 48 | threadSafety: _AssociationPolicyThreadSafety 49 | ) -> Bool { 50 | return _setAssociatedObject( 51 | object, 52 | to: self, 53 | forKey: key, 54 | threadSafety: threadSafety 55 | ) 56 | } 57 | 58 | @inlinable 59 | public func getAssociatedObject( 60 | of type: Object.Type = Object.self, 61 | forKey key: StaticString 62 | ) -> Object? { 63 | return _getAssociatedObject( 64 | forKey: key, 65 | from: self 66 | ) 67 | } 68 | } 69 | 70 | @inlinable 71 | @discardableResult 72 | public func _setAssociatedObject( 73 | _ object: Object?, 74 | to associatingObject: AnyObject, 75 | forKey key: StaticString, 76 | threadSafety: _AssociationPolicyThreadSafety = .nonatomic 77 | ) -> Bool { 78 | _setAssociatedObject( 79 | object, 80 | to: associatingObject, 81 | forKey: key, 82 | policy: .init( 83 | Object.self is AnyClass ? .retain : .copy, 84 | threadSafety 85 | ) 86 | ) 87 | } 88 | 89 | @inlinable 90 | @discardableResult 91 | public func _setAssociatedObject( 92 | _ object: Object?, 93 | to associatingObject: AnyObject, 94 | forKey key: StaticString, 95 | policy: objc_AssociationPolicy 96 | ) -> Bool { 97 | guard key.hasPointerRepresentation 98 | else { return false } 99 | 100 | objc_setAssociatedObject( 101 | associatingObject, 102 | UnsafeRawPointer(key.utf8Start), 103 | object, 104 | policy 105 | ) 106 | 107 | return true 108 | } 109 | 110 | @inlinable 111 | public func _getAssociatedObject( 112 | of type: Object.Type = Object.self, 113 | forKey key: StaticString, 114 | from associatingObject: AnyObject 115 | ) -> Object? { 116 | guard key.hasPointerRepresentation 117 | else { return nil } 118 | 119 | return objc_getAssociatedObject( 120 | associatingObject, 121 | UnsafeRawPointer(key.utf8Start) 122 | ) 123 | .flatMap { $0 as? Object } 124 | } 125 | 126 | extension NSObject: AssociatingObject {} 127 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/AssociatingObject/objc_AssociationPolicy+.swift: -------------------------------------------------------------------------------- 1 | /// Helper for creating `objc_AssociationPolicy` 2 | public enum _AssociationPolicyKind: String { 3 | /// `OBJC_ASSOCIATION_COPY` / `OBJC_ASSOCIATION_COPY_NONATOMIC` 4 | /// 5 | /// Use this policy when you need the value of the object as it was at the moment the property was set, 6 | /// and the object is possibly mutable. 7 | case copy 8 | 9 | /// `OBJC_ASSOCIATION_ASSIGN` 10 | /// 11 | /// Use this when you're associating a raw pointer, or for a weak reference to an object. 12 | /// It does not extend the lifetime of the associated object. 13 | case assign 14 | 15 | 16 | /// `OBJC_ASSOCIATION_RETAIN` / `OBJC_ASSOCIATION_RETAIN_NONATOMIC` 17 | /// 18 | /// Use this to specify a strong reference to the associated object 19 | case retain 20 | } 21 | 22 | /// Helper for creating `objc_AssociationPolicy` 23 | /// 24 | /// Remember, `.atomic` properties ensure that an entire value is set/get 25 | /// before another operation can take place on it - these are thread-safe but slower. 26 | /// On the other hand, `.nonatomic` properties don't have that restriction - they're faster but not thread-safe. 27 | public enum _AssociationPolicyThreadSafety: String { 28 | /// It is the default behaviour. If an object is declared as atomic then it becomes thread-safe. 29 | /// 30 | /// Thread-safe means, at a time only one thread of a particular instance of that class can have the control over that object. 31 | case atomic 32 | 33 | /// Disable thread-safety. it’s faster to access a nonatomic property than an atomic one. 34 | /// 35 | /// You can use the nonatomic property attribute to specify that synthesized accessors simply set or return a value directly, 36 | /// with no guarantees about what happens if that same value is accessed simultaneously from different threads. 37 | /// For this reason, 38 | case nonatomic 39 | } 40 | 41 | extension objc_AssociationPolicy { 42 | @inlinable 43 | public init( 44 | _ kind: _AssociationPolicyKind, 45 | _ threadSafety: _AssociationPolicyThreadSafety 46 | ) { 47 | switch kind { 48 | case .copy: 49 | self = .copy(threadSafety) 50 | case .assign: 51 | self = .assign 52 | case .retain: 53 | self = .retain(threadSafety) 54 | } 55 | } 56 | 57 | /// `OBJC_ASSOCIATION_ASSIGN` 58 | /// 59 | /// Use this when you're associating a raw pointer, or for a weak reference to an object. 60 | /// It does not extend the lifetime of the associated object. 61 | @inlinable 62 | public static var assign: Self { .OBJC_ASSOCIATION_ASSIGN } 63 | 64 | /// `OBJC_ASSOCIATION_RETAIN` / `OBJC_ASSOCIATION_RETAIN_NONATOMIC` 65 | /// 66 | /// Use `.retain(.atomic)` to specify a strong reference to the associated object and it's thread-safe. 67 | /// Use this when you're working in a multithreaded environment and you need to ensure that 68 | /// the associated object isn't deallocated before you're done with it. 69 | /// 70 | /// `.retain(.nonatomic)` specifies a strong reference to the associated object and is not thread-safe. 71 | /// This is appropriate when you're not concerned with performance in multithreaded scenarios. 72 | @inlinable 73 | public static func retain(_ threadSafety: _AssociationPolicyThreadSafety) -> Self { 74 | switch threadSafety { 75 | case .atomic: 76 | .OBJC_ASSOCIATION_RETAIN 77 | case .nonatomic: 78 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 79 | } 80 | } 81 | 82 | /// `OBJC_ASSOCIATION_COPY` / `OBJC_ASSOCIATION_COPY_NONATOMIC` 83 | /// 84 | /// `.copy(.atomic)` as well as `.retain` also creates a copy of the associated object, 85 | /// and the copy is made in an atomic way (thread-safe). Use this policy when 86 | /// you're working in a multithreaded environment and you need the value of the object 87 | /// as it was at the moment the property was set, and the object is possibly mutable. 88 | /// 89 | /// `.copy(.nonatomic)` creates a copy of the associated object, and the copy is made in a non-atomic way. 90 | /// Use this policy when you need the value of the object as it was at the moment the property was set, 91 | /// and the object is possibly mutable. 92 | @inlinable 93 | public static func copy(_ threadSafety: _AssociationPolicyThreadSafety) -> Self { 94 | switch threadSafety { 95 | case .atomic: 96 | .OBJC_ASSOCIATION_COPY 97 | case .nonatomic: 98 | .OBJC_ASSOCIATION_COPY_NONATOMIC 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/Box&Reference/Box.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | @dynamicMemberLookup 5 | final public class Box { 6 | public var content: Content 7 | 8 | @inlinable 9 | public var wrappedValue: Content { 10 | get { content } 11 | set { content = newValue } 12 | } 13 | 14 | @inlinable 15 | public convenience init() where Content == T? { 16 | self.init(wrappedValue: nil) 17 | } 18 | 19 | @inlinable 20 | public convenience init(_ wrappedValue: Content) { 21 | self.init(wrappedValue: wrappedValue) 22 | } 23 | 24 | public init(wrappedValue: Content) { 25 | self.content = wrappedValue 26 | } 27 | 28 | @inlinable 29 | public var projectedValue: Reference { reference } 30 | 31 | @inlinable 32 | public var reference: Reference { 33 | .object(self, keyPath: \.wrappedValue) 34 | } 35 | 36 | @inlinable 37 | public subscript(dynamicMember keyPath: KeyPath) -> U { 38 | get { self.wrappedValue[keyPath: keyPath] } 39 | } 40 | 41 | @inlinable 42 | public subscript(dynamicMember keyPath: WritableKeyPath) -> U { 43 | get { self.wrappedValue[keyPath: keyPath] } 44 | set { self.wrappedValue[keyPath: keyPath] = newValue } 45 | } 46 | 47 | @inlinable 48 | public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> U { 49 | get { self.wrappedValue[keyPath: keyPath] } 50 | set { self.wrappedValue[keyPath: keyPath] = newValue } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/Box&Reference/Reference.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(Combine) 4 | import Combine 5 | #endif 6 | 7 | extension ReadonlyReference { 8 | @inlinable 9 | public static func constant(_ value: Value) -> ReadonlyReference { 10 | ReadonlyReference { value } 11 | } 12 | 13 | @inlinable 14 | public static func object(_ object: Object, read: @escaping (Object) -> Value) 15 | -> ReadonlyReference 16 | { 17 | ReadonlyReference(read: { read(object) }) 18 | } 19 | } 20 | 21 | @propertyWrapper 22 | @dynamicMemberLookup 23 | public struct ReadonlyReference { 24 | @usableFromInline 25 | internal var _read: () -> Value 26 | 27 | @inlinable 28 | public init(wrappedValue: Value) { 29 | self.init(read: { wrappedValue }) 30 | } 31 | 32 | public init(read: @escaping () -> Value) { 33 | self._read = read 34 | } 35 | 36 | @inlinable 37 | public var wrappedValue: Value { 38 | self._read() 39 | } 40 | 41 | @inlinable 42 | public var projectedValue: ReadonlyReference { 43 | ReadonlyReference(read: _read) 44 | } 45 | 46 | @inlinable 47 | public var asWritable: Reference { 48 | Reference(read: _read, write: { _ in }) 49 | } 50 | 51 | @inlinable 52 | public func writable(with write: @escaping (Value) -> Void) -> Reference { 53 | Reference(read: _read, write: write) 54 | } 55 | 56 | @inlinable 57 | public subscript(dynamicMember keyPath: KeyPath) -> Reference< 58 | LocalValue 59 | > { 60 | .readonly { wrappedValue[keyPath: keyPath] } 61 | } 62 | 63 | @inlinable 64 | public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Reference< 65 | LocalValue 66 | > { 67 | Reference( 68 | read: { _read()[keyPath: keyPath] }, 69 | write: { _read()[keyPath: keyPath] = $0 }) 70 | } 71 | } 72 | 73 | extension Reference { 74 | @inlinable 75 | public static func readonly(_ read: @escaping () -> Value) -> Reference { 76 | Reference(read: read, write: { _ in }) 77 | } 78 | 79 | @inlinable 80 | public static func object(_ object: Object, read: @escaping (Object) -> Value) 81 | -> Reference 82 | { 83 | .readonly { read(object) } 84 | } 85 | 86 | @inlinable 87 | public static func object( 88 | _ object: Object, 89 | keyPath: ReferenceWritableKeyPath 90 | ) -> Reference { 91 | Reference( 92 | read: { object[keyPath: keyPath] }, 93 | write: { object[keyPath: keyPath] = $0 } 94 | ) 95 | } 96 | 97 | @inlinable 98 | public static func variable(_ initialValue: Value) -> Reference { 99 | Reference(wrappedValue: initialValue) 100 | } 101 | 102 | @inlinable 103 | public static func constant(_ value: Value) -> Reference { 104 | .readonly { value } 105 | } 106 | } 107 | 108 | @propertyWrapper 109 | @dynamicMemberLookup 110 | public struct Reference { 111 | @usableFromInline 112 | internal var _read: () -> Value 113 | 114 | @usableFromInline 115 | internal var _write: (Value) -> Void 116 | 117 | @inlinable 118 | public init(wrappedValue: Value) { 119 | var value = wrappedValue 120 | self.init( 121 | read: { value }, 122 | write: { value = $0 } 123 | ) 124 | } 125 | 126 | public init( 127 | read: @escaping () -> Value, 128 | write: @escaping (Value) -> Void 129 | ) { 130 | self._read = read 131 | self._write = write 132 | } 133 | 134 | @inlinable 135 | public func read() -> Value { _read() } 136 | 137 | @inlinable 138 | public func write(_ value: Value) { _write(value) } 139 | 140 | @inlinable 141 | public var wrappedValue: Value { 142 | get { _read() } 143 | nonmutating set { _write(newValue) } 144 | } 145 | 146 | @inlinable 147 | public var projectedValue: Reference { 148 | Reference(read: _read, write: _write) 149 | } 150 | 151 | @inlinable 152 | public var readonly: ReadonlyReference { ReadonlyReference(read: _read) } 153 | 154 | @inlinable 155 | public subscript( 156 | dynamicMember keyPath: KeyPath 157 | ) -> Reference { 158 | .readonly { wrappedValue[keyPath: keyPath] } 159 | } 160 | 161 | @inlinable 162 | public subscript( 163 | dynamicMember keyPath: WritableKeyPath 164 | ) -> Reference { 165 | Reference( 166 | read: { _read()[keyPath: keyPath] }, 167 | write: { localValue in 168 | var value = _read() 169 | value[keyPath: keyPath] = localValue 170 | _write(value) 171 | } 172 | ) 173 | } 174 | } 175 | 176 | extension Reference { 177 | public func map( 178 | read: @escaping (Value) -> T, 179 | write: @escaping (T) -> Value 180 | ) -> Reference { 181 | Reference( 182 | read: { read(_read()) }, 183 | write: { _write(write($0)) } 184 | ) 185 | } 186 | 187 | public func onSet(perform action: @escaping (Value) -> Void) -> Reference { 188 | Reference( 189 | read: _read, 190 | write: { newValue in 191 | _write(newValue) 192 | action(newValue) 193 | } 194 | ) 195 | } 196 | 197 | public func onChange(perform action: @escaping (Value) -> Void) -> Reference 198 | where Value: Equatable { 199 | Reference( 200 | read: _read, 201 | write: { 202 | let oldValue = _read() 203 | _write($0) 204 | let newValue = _read() 205 | if oldValue != newValue { 206 | action(newValue) 207 | } 208 | } 209 | ) 210 | } 211 | } 212 | 213 | public protocol ReferenceProvider: AnyObject {} 214 | 215 | extension ReferenceProvider { 216 | @inlinable 217 | public func reference( 218 | for keyPath: KeyPath 219 | ) -> ReadonlyReference { 220 | .object(self, read: { $0[keyPath: keyPath] }) 221 | } 222 | 223 | @inlinable 224 | public func reference( 225 | for keyPath: ReferenceWritableKeyPath 226 | ) -> Reference { 227 | .object(self, keyPath: keyPath) 228 | } 229 | } 230 | 231 | extension NSObject: ReferenceProvider {} 232 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/Castable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol Castable { 4 | func `as`(_ type: T.Type) -> T? 5 | func `is`(_ type: T.Type) -> Bool 6 | } 7 | 8 | extension Castable { 9 | public func `as`(_ type: T.Type) -> T? { 10 | self as? T 11 | } 12 | 13 | public func `is`(_ type: T.Type) -> Bool { 14 | self is T 15 | } 16 | } 17 | 18 | extension Optional: Castable {} 19 | 20 | extension NSObject: Castable {} 21 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/Equated/Equated+Comparator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Equated { 4 | public struct Comparator: @unchecked Sendable { 5 | @usableFromInline 6 | internal init(compare: @escaping (Value, Value) -> Bool) { 7 | self.compare = compare 8 | } 9 | 10 | public let compare: (Value, Value) -> Bool 11 | } 12 | } 13 | 14 | extension Equated.Comparator { 15 | @inlinable 16 | public static func custom(_ compare: @escaping (Value, Value) -> Bool) -> Self { 17 | return .init(compare: compare) 18 | } 19 | 20 | @inlinable 21 | public static func property( 22 | _ scope: @escaping (Value) -> Property 23 | ) -> Self { 24 | return .init { scope($0) == scope($1) } 25 | } 26 | 27 | @inlinable 28 | public static func wrappedProperty( 29 | _ scope: @escaping (Wrapped) -> Property 30 | ) -> Self where Value == Optional { 31 | return .init { $0.map(scope) == $1.map(scope) } 32 | } 33 | 34 | @inlinable 35 | public static var dump: Self { 36 | .init { lhs, rhs in 37 | var (lhsDump, rhsDump) = ("", "") 38 | Swift.dump(lhs, to: &lhsDump) 39 | Swift.dump(rhs, to: &rhsDump) 40 | return lhsDump == rhsDump 41 | } 42 | } 43 | 44 | @inlinable 45 | public static var typedDump: Self { 46 | .init { lhs, rhs in 47 | var (lhsDump, rhsDump) = ("\(type(of: lhs))", "\(type(of: rhs))") 48 | Swift.dump(lhs, to: &lhsDump) 49 | Swift.dump(rhs, to: &rhsDump) 50 | return lhsDump == rhsDump 51 | } 52 | } 53 | } 54 | 55 | extension Equated.Comparator where Value: Error { 56 | @inlinable 57 | public static var localizedDescription: Self { 58 | .property(\.localizedDescription) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/Equated/Equated.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | public struct Equated: Equatable { 5 | @inlinable 6 | public init(by comparator: Comparator) where Value == T? { 7 | self.init(.none, by: comparator) 8 | } 9 | 10 | @inlinable 11 | public init(_ wrappedValue: Value, by comparator: Comparator) { 12 | self.init(wrappedValue: wrappedValue, by: comparator) 13 | } 14 | 15 | @inlinable 16 | public init(wrappedValue: Value, by comparator: Comparator) { 17 | self.wrappedValue = wrappedValue 18 | self.comparator = comparator 19 | } 20 | 21 | public var wrappedValue: Value 22 | public var comparator: Comparator 23 | 24 | @inlinable 25 | public static func == (lhs: Equated, rhs: Equated) -> Bool { 26 | lhs.comparator.compare(lhs.wrappedValue, rhs.wrappedValue) 27 | && rhs.comparator.compare(rhs.wrappedValue, lhs.wrappedValue) 28 | } 29 | } 30 | 31 | extension Equated where Value: Equatable { 32 | @inlinable 33 | public init() where Value == T? { 34 | self.init(wrappedValue: .none) 35 | } 36 | 37 | @inlinable 38 | public init(wrappedValue: Value) { 39 | self.init(wrappedValue, by: .custom(==)) 40 | } 41 | } 42 | 43 | extension Equated: Error where Value: Error { 44 | @inlinable 45 | public init(_ wrappedValue: Value) { 46 | self.init(wrappedValue: wrappedValue) 47 | } 48 | 49 | @inlinable 50 | public init(wrappedValue: Value) { 51 | self.init( 52 | wrappedValue: wrappedValue, 53 | by: .localizedDescription 54 | ) 55 | } 56 | 57 | @inlinable 58 | public var localizedDescription: String { wrappedValue.localizedDescription } 59 | } 60 | 61 | extension Equated: Hashable where Value: Hashable { 62 | @inlinable 63 | public func hash(into hasher: inout Hasher) { 64 | wrappedValue.hash(into: &hasher) 65 | } 66 | } 67 | 68 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 69 | extension Equated: Identifiable where Value: Identifiable { 70 | @inlinable 71 | public var id: Value.ID { wrappedValue.id } 72 | } 73 | 74 | 75 | extension Equated: Sendable where Value: Sendable {} 76 | 77 | extension Equated: Comparable where Value: Comparable { 78 | @inlinable 79 | public static func <(lhs: Self, rhs: Self) -> Bool { 80 | lhs.wrappedValue < rhs.wrappedValue 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/Indirect.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// CoW container, that allows you to wrap structs recursively 4 | /// 5 | /// Usage: 6 | /// ```swift 7 | /// public struct User { 8 | /// internal init(id: UUID, favoriteFollower: User?) { 9 | /// self.id = id 10 | /// self._favoriteFollower = Indirect(favoriteFollower) 11 | /// } 12 | /// public var id: UUID 13 | /// 14 | /// @Indirect 15 | /// public var favoriteFollower: User? 16 | /// } 17 | /// 18 | /// var user = User(id: UUID(), favoriteFollower: User(id: UUID())) 19 | /// user.favoriteFollower?.id 20 | /// ``` 21 | /// Note: Codable stuff behaviour is not tested for propertyWrapper style, maybe u should consider 22 | /// using `var favoriteFollower: Indirect`. 23 | /// 24 | @propertyWrapper 25 | @dynamicMemberLookup 26 | public struct Indirect { 27 | @usableFromInline 28 | class Storage: @unchecked Sendable { 29 | @usableFromInline 30 | var value: Value 31 | 32 | @usableFromInline 33 | init(_ value: Value) { 34 | self.value = value 35 | } 36 | } 37 | 38 | @usableFromInline 39 | var storage: Storage 40 | 41 | @inlinable 42 | public init(_ value: Value) { 43 | self.init(wrappedValue: value) 44 | } 45 | 46 | @inlinable 47 | public init(wrappedValue: Value) { 48 | self.storage = .init(wrappedValue) 49 | } 50 | 51 | @inlinable 52 | public var wrappedValue: Value { 53 | get { storage.value } 54 | set { 55 | if !isKnownUniquelyReferenced(&storage) { 56 | storage = Storage(storage.value) 57 | } 58 | storage.value = newValue 59 | } 60 | } 61 | 62 | @inlinable 63 | public var projectedValue: Self { 64 | get { self } 65 | set { self = newValue } 66 | } 67 | 68 | @inlinable 69 | public subscript(dynamicMember keyPath: KeyPath) -> T { 70 | wrappedValue[keyPath: keyPath] 71 | } 72 | 73 | @inlinable 74 | public subscript(dynamicMember keyPath: WritableKeyPath) -> T { 75 | get { wrappedValue[keyPath: keyPath] } 76 | set { wrappedValue[keyPath: keyPath] = newValue } 77 | } 78 | 79 | /// Assigns the new value directly to internal CoW storage 80 | @inlinable 81 | public func _setValue(_ value: Value) { 82 | storage.value = value 83 | } 84 | 85 | /// Modifies internal CoW storage value directly 86 | @inlinable 87 | public func _modifyValue(_ transform: (inout Value) -> Void) { 88 | transform(&storage.value) 89 | } 90 | 91 | /// Checks if internal CoW storage is shared with other value 92 | @inlinable 93 | public func _sharesStorage(with other: Self) -> Bool { 94 | storage === other.storage 95 | } 96 | } 97 | 98 | extension Indirect: Sendable where Value: Sendable {} 99 | 100 | extension Indirect: Equatable where Value: Equatable { 101 | @inlinable 102 | public static func ==(lhs: Self, rhs: Self) -> Bool { 103 | lhs.wrappedValue == rhs.wrappedValue 104 | } 105 | } 106 | 107 | extension Indirect: Hashable where Value: Hashable { 108 | @inlinable 109 | public func hash(into hasher: inout Hasher) { 110 | wrappedValue.hash(into: &hasher) 111 | } 112 | } 113 | 114 | extension Indirect: Comparable where Value: Comparable { 115 | @inlinable 116 | public static func <(lhs: Self, rhs: Self) -> Bool { 117 | lhs.wrappedValue < rhs.wrappedValue 118 | } 119 | } 120 | 121 | extension Indirect: Decodable where Value: Decodable { 122 | @inlinable 123 | public init(from decoder: Decoder) throws { 124 | try self.init(.init(from: decoder)) 125 | } 126 | } 127 | 128 | extension Indirect: Encodable where Value: Encodable { 129 | @inlinable 130 | public func encode(to encoder: Encoder) throws { 131 | try wrappedValue.encode(to: encoder) 132 | } 133 | } 134 | 135 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 136 | extension Indirect: Identifiable where Value: Identifiable { 137 | @inlinable 138 | public var id: Value.ID { wrappedValue.id } 139 | } 140 | 141 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/PropertyProxy.swift: -------------------------------------------------------------------------------- 1 | // https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/ 2 | 3 | import FunctionalKeyPath 4 | import Foundation 5 | 6 | @propertyWrapper 7 | public struct PropertyProxy { 8 | public static subscript( 9 | _enclosingInstance instance: Object, 10 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 11 | storage storageKeyPath: ReferenceWritableKeyPath 12 | ) -> Value { 13 | get { 14 | let path = instance[keyPath: storageKeyPath].path 15 | return path.extract(from: instance) 16 | } 17 | set { 18 | let wrapper = instance[keyPath: storageKeyPath] 19 | _ = wrapper.path.embed(newValue, in: instance) 20 | } 21 | } 22 | 23 | private let path: FunctionalKeyPath 24 | 25 | @available(*, unavailable, message: "@ObjectProxy can only be applied to classes") 26 | public var wrappedValue: Value { 27 | get { fatalError() } 28 | set { fatalError() } 29 | } 30 | 31 | public init(_ path: FunctionalKeyPath) { 32 | self.path = path 33 | } 34 | 35 | public init(_ keyPath: ReferenceWritableKeyPath) { 36 | self.path = .init(keyPath) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/Resettable/Resettable+CollectionProxy.swift: -------------------------------------------------------------------------------- 1 | import FunctionalKeyPath 2 | 3 | extension Resettable where Object: Collection { 4 | public var collection: WritableCollectionProxy { 5 | WritableCollectionProxy( 6 | resettable: self, 7 | keyPath: .init( 8 | embed: { value, root in 9 | return value 10 | }, extract: { root in 11 | return root 12 | } 13 | ) 14 | ) 15 | } 16 | } 17 | 18 | extension Resettable { 19 | public struct WritableCollectionProxy where Collection: Swift.Collection { 20 | @usableFromInline 21 | internal init( 22 | resettable: Resettable, 23 | keyPath: FunctionalKeyPath 24 | ) { 25 | self.resettable = resettable 26 | self.keyPath = keyPath 27 | } 28 | 29 | var resettable: Resettable 30 | var keyPath: FunctionalKeyPath 31 | 32 | public subscript(_ idx: Collection.Index) -> KeyPathContainer { 33 | return KeyPathContainer( 34 | resettable: resettable, 35 | keyPath: keyPath.appending(path: .getonlyIndex(idx)) 36 | ) 37 | } 38 | 39 | public subscript(_ idx: Collection.Index) -> WritableKeyPathContainer 40 | where Collection: Swift.MutableCollection { 41 | return WritableKeyPathContainer( 42 | resettable: resettable, 43 | keyPath: keyPath.appending(path: .index(idx)) 44 | ) 45 | } 46 | 47 | public subscript(safe idx: Collection.Index) -> WritableKeyPathContainer 48 | where Collection == Array { 49 | return WritableKeyPathContainer( 50 | resettable: resettable, 51 | keyPath: keyPath.appending(path: FunctionalKeyPath.safeIndex(idx)) 52 | ) 53 | } 54 | } 55 | 56 | public struct CollectionProxy where Collection: Swift.Collection { 57 | @usableFromInline 58 | internal init( 59 | resettable: Resettable, 60 | keyPath: FunctionalKeyPath 61 | ) { 62 | self.resettable = resettable 63 | self.keyPath = keyPath 64 | } 65 | 66 | var resettable: Resettable 67 | var keyPath: FunctionalKeyPath 68 | 69 | public subscript(_ idx: Collection.Index) -> KeyPathContainer { 70 | return KeyPathContainer( 71 | resettable: resettable, 72 | keyPath: keyPath.appending(path: .getonlyIndex(idx)) 73 | ) 74 | } 75 | 76 | public subscript(safe idx: Collection.Index) -> KeyPathContainer 77 | where Collection == Array { 78 | return KeyPathContainer( 79 | resettable: resettable, 80 | keyPath: keyPath.appending(path: FunctionalKeyPath.safeIndex(idx)) 81 | ) 82 | } 83 | } 84 | } 85 | 86 | extension Resettable.WritableCollectionProxy { 87 | @discardableResult 88 | public func swapAt(_ idx1: Collection.Index, _ idx2: Collection.Index, operation: Resettable.OperationBehavior = .default) -> Resettable 89 | where Collection == Array { 90 | resettable._modify( 91 | operation: operation, 92 | keyPath, 93 | using: { $0.swapAt(idx1, idx2) }, 94 | undo: { $0.swapAt(idx1, idx2) } 95 | ) 96 | } 97 | 98 | @discardableResult 99 | public func remove(at idx: Collection.Index, operation: Resettable.OperationBehavior = .default) -> Resettable 100 | where Collection == Array { 101 | let valueSnapshot = keyPath.extract(from: resettable.wrappedValue)[idx] 102 | return resettable._modify( 103 | operation: operation, 104 | keyPath, 105 | using: { $0.remove(at: idx) }, 106 | undo: { $0.insert(valueSnapshot, at: idx) } 107 | ) 108 | } 109 | 110 | @discardableResult 111 | public func insert(_ element: Collection.Element, at idx: Collection.Index, operation: Resettable.OperationBehavior = .default) -> Resettable 112 | where Collection == Array { 113 | return resettable._modify( 114 | operation: operation, 115 | keyPath, 116 | using: { $0.insert(element, at: idx) }, 117 | undo: { $0.remove(at: idx) } 118 | ) 119 | } 120 | 121 | @discardableResult 122 | public func append(_ element: Collection.Element, operation: Resettable.OperationBehavior = .default) -> Resettable 123 | where Collection == Array { 124 | return resettable._modify( 125 | operation: operation, 126 | keyPath, 127 | using: { $0.append(element) }, 128 | undo: { $0.removeLast() } 129 | ) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/Resettable/Resettable.swift: -------------------------------------------------------------------------------- 1 | import CustomDump 2 | import FunctionalKeyPath 3 | 4 | extension Resettable { 5 | @available( 6 | *, deprecated, 7 | message: """ 8 | Might be removed due to Value type constraint, 9 | which makes the behavior unstable, 10 | consider re-implementing this feature locally if you need 11 | """ 12 | ) 13 | public struct ValuesDump { 14 | @usableFromInline 15 | internal init( 16 | items: [Object], 17 | currentIndex: Int 18 | ) { 19 | self.items = items 20 | self.currentIndex = currentIndex 21 | } 22 | 23 | public let items: [Object] 24 | public let currentIndex: Int 25 | } 26 | } 27 | 28 | extension Resettable { 29 | public enum OperationBehavior { 30 | case `default` 31 | case amend 32 | case insert 33 | case inject 34 | } 35 | } 36 | 37 | @propertyWrapper 38 | @dynamicMemberLookup 39 | public class Resettable { 40 | @inlinable 41 | public init(_ object: Object) { 42 | self.object = object 43 | self.pointer = Pointer(undo: nil, redo: nil) 44 | } 45 | 46 | @usableFromInline 47 | internal var object: Object 48 | 49 | @inlinable 50 | public var wrappedValue: Object { object } 51 | 52 | @inlinable 53 | public var projectedValue: Resettable { self } 54 | 55 | @usableFromInline 56 | internal var pointer: Pointer 57 | 58 | // MARK: - Undo/Redo 59 | 60 | /// Dump values for __ValueTypes__ 61 | @available( 62 | *, deprecated, 63 | message: "Might be removed due to Value type constraint, which makes the behavior unstable, consider re-implementing this feature locally if you need" 64 | ) 65 | @inlinable 66 | public func valuesDump() -> ValuesDump { 67 | let _pointer = pointer 68 | while pointer !== undo().pointer {} 69 | var buffer: [Object] = [wrappedValue] 70 | var indexBuffer = 0 71 | var currentIndex = 0 72 | 73 | while pointer !== redo().pointer { 74 | indexBuffer += 1 75 | let isCurrent = _pointer === pointer 76 | buffer.append(wrappedValue) 77 | if isCurrent { currentIndex = indexBuffer } 78 | } 79 | 80 | if _pointer !== pointer { 81 | while _pointer !== undo().pointer {} 82 | } 83 | 84 | return ValuesDump(items: buffer, currentIndex: currentIndex) 85 | } 86 | 87 | @inlinable 88 | public func dump() -> String { 89 | var buffer = "" 90 | self.dump(to: &buffer) 91 | return buffer 92 | } 93 | 94 | @inlinable 95 | public func dump( 96 | to stream: inout TargetStream 97 | ) { 98 | let _pointer = pointer 99 | while pointer !== undo().pointer {} 100 | var buffer: [String] = [] 101 | var initialBuffer = "" 102 | customDump(wrappedValue, to: &initialBuffer) 103 | buffer.append(#"""""#) 104 | buffer.append("\n") 105 | buffer.append( 106 | initialBuffer.components(separatedBy: .newlines) 107 | .map { " " + $0 } 108 | .joined(separator: "\n") 109 | ) 110 | buffer.append("\n") 111 | 112 | var previous = wrappedValue 113 | 114 | while pointer !== redo().pointer { 115 | let isCurrent = _pointer === pointer 116 | var dump = "" 117 | customDump(diff(previous, wrappedValue), to: &dump) 118 | if dump == "nil" { 119 | buffer.append("\n No state changes \n") 120 | } else { 121 | var _dump = dump.trimmingCharacters(in: [#"""#]) 122 | if isCurrent { 123 | if _dump.hasPrefix("\n ") { 124 | _dump.removeFirst(3) 125 | } 126 | buffer.append("\n >>> ".appending(_dump)) 127 | } else { 128 | buffer.append(_dump) 129 | } 130 | } 131 | previous = wrappedValue 132 | } 133 | 134 | buffer.append(#"""""#) 135 | 136 | stream.write(buffer.joined()) 137 | 138 | if _pointer !== pointer { 139 | while _pointer !== undo().pointer {} 140 | } 141 | } 142 | 143 | @discardableResult 144 | @inlinable 145 | public func undo() -> Resettable { 146 | pointer = pointer.undo(&object) 147 | return self 148 | } 149 | 150 | @discardableResult 151 | @inlinable 152 | public func redo() -> Resettable { 153 | pointer = pointer.redo(&object) 154 | return self 155 | } 156 | 157 | @discardableResult 158 | @inlinable 159 | public func undo(_ count: Int) -> Resettable { 160 | for _ in 0.. Resettable { 167 | for _ in 0.. Resettable { 174 | while pointer !== undo().pointer {} 175 | return self 176 | } 177 | 178 | @discardableResult 179 | @inlinable 180 | public func restore() -> Resettable { 181 | while pointer !== redo().pointer {} 182 | return self 183 | } 184 | 185 | // MARK: - Unsafe modification 186 | 187 | @discardableResult 188 | @usableFromInline 189 | internal func __modify( 190 | _ nextPointer: () -> Pointer 191 | ) -> Resettable { 192 | self.pointer = nextPointer() 193 | return self 194 | } 195 | 196 | @discardableResult 197 | @inlinable 198 | public func _modify( 199 | operation: OperationBehavior = .default, 200 | _ keyPath: FunctionalKeyPath, 201 | using action: @escaping (inout Value) -> Void 202 | ) -> Resettable { 203 | __modify { 204 | pointer.apply( 205 | modification: action, 206 | for: &object, keyPath, 207 | operation: operation 208 | ) 209 | } 210 | } 211 | 212 | @discardableResult 213 | @inlinable 214 | public func _modify( 215 | operation: OperationBehavior = .default, 216 | _ keyPath: FunctionalKeyPath, 217 | using action: @escaping (inout Value) -> Void, 218 | undo: @escaping (inout Value) -> Void 219 | ) -> Resettable { 220 | __modify { 221 | pointer.apply( 222 | modification: action, 223 | for: &object, keyPath, 224 | undo: undo, 225 | operation: operation 226 | ) 227 | } 228 | } 229 | 230 | @discardableResult 231 | @inlinable 232 | public func _modify( 233 | operation: OperationBehavior = .default, 234 | using action: @escaping (inout Object) -> Void, 235 | undo: @escaping (inout Object) -> Void 236 | ) -> Resettable { 237 | __modify { 238 | pointer.apply( 239 | modification: action, 240 | undo: undo, 241 | for: &object, 242 | operation: operation 243 | ) 244 | } 245 | } 246 | 247 | // MARK: - DynamicMemberLookup 248 | 249 | // MARK: Default 250 | 251 | @inlinable 252 | public subscript( 253 | dynamicMember keyPath: WritableKeyPath 254 | ) -> WritableKeyPathContainer { 255 | WritableKeyPathContainer( 256 | resettable: self, 257 | keyPath: .init(keyPath) 258 | ) 259 | } 260 | 261 | @inlinable 262 | public subscript( 263 | dynamicMember keyPath: KeyPath 264 | ) -> KeyPathContainer { 265 | KeyPathContainer( 266 | resettable: self, 267 | keyPath: .getonly(keyPath) 268 | ) 269 | } 270 | 271 | // MARK: Optional 272 | 273 | @inlinable 274 | public subscript( 275 | dynamicMember keyPath: WritableKeyPath 276 | ) -> WritableKeyPathContainer where Object == Optional { 277 | WritableKeyPathContainer( 278 | resettable: self, 279 | keyPath: FunctionalKeyPath(keyPath).optional() 280 | ) 281 | } 282 | 283 | @inlinable 284 | public subscript( 285 | dynamicMember keyPath: KeyPath 286 | ) -> KeyPathContainer where Object == Optional { 287 | KeyPathContainer( 288 | resettable: self, 289 | keyPath: FunctionalKeyPath.getonly(keyPath).optional() 290 | ) 291 | } 292 | 293 | // MARK: Collection 294 | 295 | @inlinable 296 | public subscript( 297 | dynamicMember keyPath: WritableKeyPath 298 | ) -> WritableCollectionProxy where Value: Swift.Collection { 299 | WritableCollectionProxy( 300 | resettable: self, 301 | keyPath: .init(keyPath) 302 | ) 303 | } 304 | 305 | @inlinable 306 | public subscript( 307 | dynamicMember keyPath: KeyPath 308 | ) -> CollectionProxy where Value: Swift.Collection { 309 | CollectionProxy( 310 | resettable: self, 311 | keyPath: .getonly(keyPath) 312 | ) 313 | } 314 | } 315 | 316 | // MARK: - Undo/Redo Core 317 | 318 | extension Resettable { 319 | @usableFromInline 320 | internal class Pointer { 321 | @usableFromInline 322 | init( 323 | prev: Pointer? = nil, 324 | next: Pointer? = nil, 325 | undo: ((inout Object) -> Void)? = nil, 326 | redo: ((inout Object) -> Void)? = nil 327 | ) { 328 | self.prev = prev 329 | self.next = next 330 | self._undo = undo 331 | self._redo = redo 332 | } 333 | 334 | @usableFromInline 335 | var prev: Pointer? 336 | 337 | @usableFromInline 338 | var next: Pointer? 339 | 340 | @usableFromInline 341 | var _undo: ((inout Object) -> Void)? 342 | 343 | @usableFromInline 344 | var _redo: ((inout Object) -> Void)? 345 | 346 | // MARK: - Undo/Redo 347 | 348 | @usableFromInline 349 | func undo(_ object: inout Object) -> Pointer { 350 | _undo?(&object) 351 | return prev.or(self) 352 | } 353 | 354 | @usableFromInline 355 | func redo(_ object: inout Object) -> Pointer { 356 | _redo?(&object) 357 | return next.or(self) 358 | } 359 | 360 | // MARK: - Apply 361 | 362 | @usableFromInline 363 | func apply( 364 | modification action: @escaping (inout Value) -> Void, 365 | for object: inout Object, 366 | _ keyPath: FunctionalKeyPath, 367 | operation: OperationBehavior = .default 368 | ) -> Pointer { 369 | var didPrepareObjectForAmend = false 370 | if operation == .amend { 371 | self._undo?(&object) 372 | didPrepareObjectForAmend = true 373 | } 374 | let valueSnapshot = keyPath.extract(from: object) 375 | return apply( 376 | modification: action, 377 | for: &object, 378 | keyPath, 379 | undo: { $0 = valueSnapshot }, 380 | operation: operation, 381 | didPrepareObjectForAmend: didPrepareObjectForAmend 382 | ) 383 | } 384 | 385 | @usableFromInline 386 | func apply( 387 | modification action: @escaping (inout Value) -> Void, 388 | for object: inout Object, 389 | _ keyPath: FunctionalKeyPath, 390 | undo: @escaping (inout Value) -> Void, 391 | operation: OperationBehavior = .default, 392 | didPrepareObjectForAmend: Bool = false 393 | ) -> Pointer { 394 | return apply( 395 | modification: { object in 396 | keyPath.embed( 397 | modification( 398 | of: keyPath.extract(from: object), 399 | with: action 400 | ), 401 | in: &object 402 | ) 403 | }, 404 | undo: { object in 405 | keyPath.embed( 406 | modification( 407 | of: keyPath.extract(from: object), 408 | with: undo 409 | ), 410 | in: &object 411 | ) 412 | }, 413 | for: &object, 414 | operation: operation, 415 | didPrepareObjectForAmend: didPrepareObjectForAmend 416 | ) 417 | } 418 | 419 | @usableFromInline 420 | func apply( 421 | modification: @escaping (inout Object) -> Void, 422 | undo: @escaping (inout Object) -> Void, 423 | for object: inout Object, 424 | operation: OperationBehavior = .default, 425 | didPrepareObjectForAmend: Bool = false 426 | ) -> Pointer { 427 | if operation == .inject { 428 | modification(&object) 429 | 430 | let prevUndo = self._undo 431 | self._undo = { object in 432 | undo(&object) 433 | prevUndo?(&object) 434 | } 435 | 436 | let prevRedo = self.prev?._redo 437 | self.prev?._redo = { object in 438 | prevRedo?(&object) 439 | modification(&object) 440 | } 441 | 442 | return self 443 | } 444 | 445 | if operation == .amend { 446 | if !didPrepareObjectForAmend { 447 | self._undo?(&object) 448 | } 449 | modification(&object) 450 | self._undo = undo 451 | self.prev?._redo = modification 452 | return self 453 | } 454 | 455 | let pointer = Pointer( 456 | prev: self, 457 | next: operation == .insert ? self.next : nil, 458 | undo: undo, 459 | redo: operation == .insert ? self._redo : nil 460 | ) 461 | 462 | modification(&object) 463 | self.next = pointer 464 | self._redo = modification 465 | 466 | return pointer 467 | } 468 | } 469 | } 470 | 471 | // MARK: Modification public API 472 | 473 | extension Resettable { 474 | @dynamicMemberLookup 475 | public struct KeyPathContainer { 476 | @usableFromInline 477 | internal init( 478 | resettable: Resettable, 479 | keyPath: FunctionalKeyPath 480 | ) { 481 | self.resettable = resettable 482 | self.keyPath = keyPath 483 | } 484 | 485 | @usableFromInline 486 | let resettable: Resettable 487 | 488 | @usableFromInline 489 | let keyPath: FunctionalKeyPath 490 | 491 | // MARK: - DynamicMemberLookup 492 | 493 | // MARK: Default 494 | 495 | @inlinable 496 | public subscript( 497 | dynamicMember keyPath: ReferenceWritableKeyPath 498 | ) -> WritableKeyPathContainer { 499 | WritableKeyPathContainer( 500 | resettable: resettable, 501 | keyPath: self.keyPath.appending(path: .init(keyPath)) 502 | ) 503 | } 504 | 505 | @inlinable 506 | public subscript( 507 | dynamicMember keyPath: KeyPath 508 | ) -> KeyPathContainer { 509 | KeyPathContainer( 510 | resettable: resettable, 511 | keyPath: self.keyPath.appending(path: .getonly(keyPath)) 512 | ) 513 | } 514 | 515 | // MARK: Optional 516 | 517 | @inlinable 518 | public subscript( 519 | dynamicMember keyPath: ReferenceWritableKeyPath 520 | ) -> WritableKeyPathContainer where Value == Optional { 521 | WritableKeyPathContainer( 522 | resettable: resettable, 523 | keyPath: self.keyPath.appending(path: .init(keyPath)) 524 | ) 525 | } 526 | 527 | @inlinable 528 | public subscript( 529 | dynamicMember keyPath: KeyPath 530 | ) -> KeyPathContainer where Value == Optional { 531 | KeyPathContainer( 532 | resettable: resettable, 533 | keyPath: self.keyPath.appending(path: .getonly(keyPath)) 534 | ) 535 | } 536 | 537 | // MARK: Collection 538 | 539 | @inlinable 540 | public subscript( 541 | dynamicMember keyPath: ReferenceWritableKeyPath 542 | ) -> WritableCollectionProxy where LocalValue: Swift.Collection { 543 | WritableCollectionProxy( 544 | resettable: resettable, 545 | keyPath: self.keyPath.appending(path: .init(keyPath)) 546 | ) 547 | } 548 | 549 | @inlinable 550 | public subscript( 551 | dynamicMember keyPath: KeyPath 552 | ) -> CollectionProxy where LocalValue: Swift.Collection { 553 | CollectionProxy( 554 | resettable: resettable, 555 | keyPath: self.keyPath.appending(path: .getonly(keyPath)) 556 | ) 557 | } 558 | } 559 | 560 | @dynamicMemberLookup 561 | public struct WritableKeyPathContainer { 562 | @usableFromInline 563 | internal init( 564 | resettable: Resettable, 565 | keyPath: FunctionalKeyPath 566 | ) { 567 | self.resettable = resettable 568 | self.keyPath = keyPath 569 | } 570 | 571 | @usableFromInline 572 | let resettable: Resettable 573 | 574 | @usableFromInline 575 | let keyPath: FunctionalKeyPath 576 | 577 | // MARK: Modification 578 | 579 | @discardableResult 580 | @inlinable 581 | public func callAsFunction(_ value: Value, operation: OperationBehavior = .default) -> Resettable { 582 | return self.callAsFunction(operation) { $0 = value } 583 | } 584 | 585 | @discardableResult 586 | @inlinable 587 | public func callAsFunction(_ operation: OperationBehavior = .default, _ action: @escaping (inout Value) -> Void) -> Resettable { 588 | return resettable._modify(operation: operation, keyPath, using: action) 589 | } 590 | 591 | @discardableResult 592 | @inlinable 593 | public func callAsFunction( 594 | _ operation: OperationBehavior = .default, 595 | _ action: @escaping (inout Value) -> Void, 596 | undo: @escaping (inout Value) -> Void 597 | ) -> Resettable { 598 | return resettable._modify(operation: operation, keyPath, using: action, undo: undo) 599 | } 600 | 601 | 602 | // MARK: - DynamicMemberLookup 603 | 604 | // MARK: Default 605 | 606 | @inlinable 607 | public subscript( 608 | dynamicMember keyPath: WritableKeyPath 609 | ) -> WritableKeyPathContainer { 610 | WritableKeyPathContainer( 611 | resettable: resettable, 612 | keyPath: self.keyPath.appending(path: .init(keyPath)) 613 | ) 614 | } 615 | 616 | @inlinable 617 | public subscript( 618 | dynamicMember keyPath: KeyPath 619 | ) -> KeyPathContainer { 620 | KeyPathContainer( 621 | resettable: resettable, 622 | keyPath: self.keyPath.appending(path: .getonly(keyPath)) 623 | ) 624 | } 625 | 626 | // MARK: Optional 627 | 628 | @inlinable 629 | public subscript( 630 | dynamicMember keyPath: WritableKeyPath 631 | ) -> WritableKeyPathContainer where Value == Optional { 632 | WritableKeyPathContainer( 633 | resettable: resettable, 634 | keyPath: self.keyPath.appending(path: .init(keyPath)) 635 | ) 636 | } 637 | 638 | @inlinable 639 | public subscript( 640 | dynamicMember keyPath: KeyPath 641 | ) -> KeyPathContainer where Value == Optional { 642 | KeyPathContainer( 643 | resettable: resettable, 644 | keyPath: self.keyPath.appending(path: .getonly(keyPath)) 645 | ) 646 | } 647 | 648 | // MARK: Collection 649 | 650 | @inlinable 651 | public subscript( 652 | dynamicMember keyPath: WritableKeyPath 653 | ) -> WritableCollectionProxy where LocalValue: Swift.Collection { 654 | WritableCollectionProxy( 655 | resettable: resettable, 656 | keyPath: self.keyPath.appending(path: .init(keyPath)) 657 | ) 658 | } 659 | 660 | @inlinable 661 | public subscript( 662 | dynamicMember keyPath: KeyPath 663 | ) -> CollectionProxy where LocalValue: Swift.Collection { 664 | CollectionProxy( 665 | resettable: resettable, 666 | keyPath: self.keyPath.appending(path: .getonly(keyPath)) 667 | ) 668 | } 669 | } 670 | } 671 | 672 | @discardableResult 673 | internal func modification( 674 | of object: T, 675 | with action: (inout T) -> Void 676 | ) -> T { 677 | var _object = object 678 | action(&_object) 679 | return _object 680 | } 681 | 682 | @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) 683 | extension Resettable: Identifiable where Object: Identifiable { 684 | @inlinable 685 | public var id: Object.ID { wrappedValue.id } 686 | } 687 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/RuntimeWarnings.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Notification.Name { 4 | public static let foundationExtensionsRuntimeWarning: Self = .init( 5 | rawValue: "FoundationExtensions.runtimeWarning" 6 | ) 7 | } 8 | 9 | @_transparent 10 | @inlinable 11 | @inline(__always) 12 | public func runtimeWarn( 13 | _ message: @autoclosure () -> String, 14 | category: String? = "FoundationExtensions", 15 | notificationName: Notification.Name? = .foundationExtensionsRuntimeWarning 16 | ) { 17 | #if DEBUG 18 | let message = message() 19 | notificationName.map { notificationName in 20 | NotificationCenter.default.post( 21 | name: notificationName, 22 | object: nil, 23 | userInfo: ["message": message] 24 | ) 25 | } 26 | 27 | let category = category ?? "Runtime Warning" 28 | 29 | if _XCTIsTesting { 30 | XCTFail(message) 31 | } else { 32 | #if canImport(os) 33 | os_log( 34 | .fault, 35 | dso: dso, 36 | log: OSLog(subsystem: "com.apple.runtime-issues", category: category), 37 | "%@", 38 | message 39 | ) 40 | #else 41 | fputs("\(formatter.string(from: Date())) [\(category)] \(message)\n", stderr) 42 | #endif 43 | } 44 | #endif 45 | } 46 | 47 | #if DEBUG 48 | import XCTestDynamicOverlay 49 | 50 | #if canImport(os) 51 | import os 52 | 53 | // NB: Xcode runtime warnings offer a much better experience than traditional assertions and 54 | // breakpoints, but Apple provides no means of creating custom runtime warnings ourselves. 55 | // To work around this, we hook into SwiftUI's runtime issue delivery mechanism, instead. 56 | // 57 | // Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc 58 | @usableFromInline 59 | let dso = { () -> UnsafeMutableRawPointer in 60 | let count = _dyld_image_count() 61 | for i in 0.. For swift classes consider using Swift swizzling with 25 | /// > `@_dynamicReplacement`, but keep in mind that Swift 26 | /// > swizzling causes infinite recursion for objc methods and `async` functions 27 | /// > 28 | /// > Forum: 29 | /// > - [@_dynamicReplacement causes infinite recursion]( 30 | /// https://forums.swift.org/t/dynamicreplacement-causes-infinite-recursion/52768 31 | /// ) 32 | /// > 33 | /// > Swift issues: 34 | /// > - [@_dynamicReplacement could not call origin async method]( 35 | /// https://github.com/apple/swift/issues/62214 36 | /// ) 37 | /// > - [@_dynamicReplacement can't call the original method]( 38 | /// https://github.com/apple/swift/issues/53916 39 | /// ) 40 | /// 41 | public static func objc_exchangeImplementations( 42 | _ originalSelector: Selector, 43 | _ swizzledSelector: Selector 44 | ) { 45 | let originalMethod = class_getInstanceMethod( 46 | Self.self, 47 | originalSelector 48 | ) 49 | 50 | let swizzledMethod = class_getInstanceMethod( 51 | Self.self, 52 | swizzledSelector 53 | ) 54 | 55 | guard 56 | let originalMethod, 57 | let swizzledMethod 58 | else { return } 59 | 60 | method_exchangeImplementations(originalMethod, swizzledMethod) 61 | } 62 | } 63 | 64 | extension NSObject: NSObjectSwizzlingProtocol {} 65 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/USID.swift: -------------------------------------------------------------------------------- 1 | /// Universal String Identifier 2 | /// 3 | /// Wraps string value as an identifier 4 | public struct USID: 5 | Equatable, 6 | Hashable, 7 | RawRepresentable, 8 | ExpressibleByStringLiteral, 9 | ExpressibleByStringInterpolation, 10 | LosslessStringConvertible 11 | { 12 | public let rawValue: String 13 | 14 | @inlinable 15 | public init(_ uuid: UUID = .init()) { 16 | self.init(uuid.uuidString) 17 | } 18 | 19 | @inlinable 20 | public init(stringLiteral value: String) { 21 | self.init(value) 22 | } 23 | 24 | @inlinable 25 | public init(_ value: String) { 26 | self.init(rawValue: value) 27 | } 28 | 29 | @inlinable 30 | public init(usidString value: String) { 31 | self.init(rawValue: value) 32 | } 33 | 34 | @inlinable 35 | public init(rawValue value: String) { 36 | self.rawValue = value 37 | } 38 | 39 | @inlinable 40 | public var description: String { rawValue } 41 | 42 | @inlinable 43 | public var usidString: String { rawValue } 44 | 45 | @inlinable 46 | public var intValue: Int? { Int(rawValue) } 47 | 48 | @inlinable 49 | public var uuidValue: UUID? { UUID(uuidString: rawValue) } 50 | } 51 | 52 | extension USID: Codable { 53 | @inlinable 54 | public init(from decoder: Decoder) throws { 55 | let container = try decoder.singleValueContainer() 56 | self.init(rawValue: try container.decode(String.self)) 57 | } 58 | 59 | @inlinable 60 | public func encode(to encoder: Encoder) throws { 61 | var container = encoder.singleValueContainer() 62 | try container.encode(rawValue) 63 | } 64 | } 65 | 66 | extension USID: ExpressibleByIntegerLiteral { 67 | @inlinable 68 | public init(integerLiteral value: Int) { 69 | self = .describing(value) 70 | } 71 | } 72 | 73 | extension USID { 74 | @inlinable 75 | public static func describing(_ value: Value) -> USID { 76 | return USID(rawValue: String(describing: value)) 77 | } 78 | 79 | @inlinable 80 | public static func hash(of value: Value) -> USID { 81 | return .describing(value.hashValue) 82 | } 83 | 84 | @inlinable 85 | public static func dump(_ value: T) -> USID { 86 | var buffer = "\(type(of: value))\n" 87 | Swift.dump(value, to: &buffer) 88 | return USID(rawValue: buffer) 89 | } 90 | } 91 | 92 | extension String { 93 | @inlinable 94 | public func usid() -> USID { .init(self) } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/FoundationExtensions/General/UnwrappingError.swift: -------------------------------------------------------------------------------- 1 | public struct UnwrappingError: Error { 2 | public let type: T.Type 3 | public let function: String 4 | public let file: String 5 | public let line: Int 6 | 7 | public init( 8 | _ type: T.Type = T.self, 9 | function: String = #function, 10 | file: String = #file, 11 | line: Int = #line 12 | ) { 13 | self.type = type 14 | self.function = function 15 | self.file = file 16 | self.line = line 17 | } 18 | 19 | @inlinable 20 | public var localizedDescription: String { 21 | "Could not unwrap value of type \(type)." 22 | } 23 | 24 | @inlinable 25 | public var debugDescription: String { 26 | localizedDescription 27 | .appending("\n{") 28 | .appending("\n function: \(function)") 29 | .appending("\n file: \(file),") 30 | .appending("\n line: \(line)") 31 | .appending("\n}") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/FoundationExtensionsMacros/AssociatedObject.swift: -------------------------------------------------------------------------------- 1 | @attached(accessor) 2 | public macro AssociatedObject( 3 | policy: objc_AssociationPolicy, 4 | readonly: Bool 5 | ) = #externalMacro( 6 | module: "FoundationExtensionsMacrosPlugin", 7 | type: "AssociatedObjectMacro" 8 | ) 9 | 10 | @attached(accessor) 11 | public macro AssociatedObject( 12 | threadSafety: _AssociationPolicyThreadSafety = .nonatomic, 13 | readonly: Bool = true 14 | ) = #externalMacro( 15 | module: "FoundationExtensionsMacrosPlugin", 16 | type: "AssociatedObjectMacro" 17 | ) 18 | -------------------------------------------------------------------------------- /Sources/FoundationExtensionsMacros/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import FoundationExtensions 2 | -------------------------------------------------------------------------------- /Sources/FoundationExtensionsMacrosPlugin/AssociatedObjectMacro/AssociatedObjectMacro.swift: -------------------------------------------------------------------------------- 1 | import MacroToolkit 2 | import SwiftDiagnostics 3 | import SwiftSyntax 4 | import SwiftSyntaxMacros 5 | import Foundation 6 | 7 | public struct AssociatedObjectMacro: AccessorMacro { 8 | public static func expansion( 9 | of node: AttributeSyntax, 10 | providingAccessorsOf declaration: some DeclSyntaxProtocol, 11 | in context: some MacroExpansionContext 12 | ) throws -> [AccessorDeclSyntax] { 13 | guard let decl = Decl(declaration).asVariable else { 14 | return context.diagnose(.requiresComputedProperty(declaration), return: []) 15 | } 16 | 17 | guard 18 | let binding = destructureSingle(decl.bindings), 19 | decl._syntax.bindingSpecifier.tokenKind != .keyword(.let), 20 | let name = binding.identifier 21 | else { 22 | return context.diagnose(.requiresComputedProperty(decl._syntax.bindingSpecifier), return: []) 23 | } 24 | 25 | guard let type = binding.type else { 26 | return context.diagnose(.requiresExplicitType(binding._syntax), return: []) 27 | } 28 | 29 | do { // Unsupported accessors 30 | let getter = binding.accessors.first( 31 | where: \.accessorSpecifier.tokenKind == .keyword(.get) 32 | ) 33 | 34 | if let getter { 35 | return context.diagnose( 36 | .unexpectedGetAccessor(getter.accessorSpecifier), 37 | return: [] 38 | ) 39 | } 40 | 41 | let setter = binding.accessors.first( 42 | where: \.accessorSpecifier.tokenKind == .keyword(.set) 43 | ) 44 | 45 | if let setter { 46 | return context.diagnose(.unexpectedSetAccessor(setter.accessorSpecifier), return: []) 47 | } 48 | } 49 | 50 | let willSetHandler = binding.accessors.first( 51 | where: \.accessorSpecifier.tokenKind == .keyword(.willSet) 52 | ) 53 | 54 | let didSetHandler = binding.accessors.first( 55 | where: \.accessorSpecifier.tokenKind == .keyword(.didSet) 56 | ) 57 | 58 | var setAssociatedObjectFunc: CodeBlockSyntax? = nil 59 | 60 | switch node.arguments { 61 | case .none: 62 | setAssociatedObjectFunc = CodeBlockSyntax { 63 | """ 64 | do { 65 | _setAssociatedObject( 66 | newValue, 67 | to: self, 68 | forKey: #function 69 | ) 70 | } 71 | """ 72 | } 73 | 74 | case let .argumentList(args) where args.count == 0: 75 | setAssociatedObjectFunc = CodeBlockSyntax { 76 | """ 77 | do { 78 | _setAssociatedObject( 79 | newValue, 80 | to: self, 81 | forKey: #function 82 | ) 83 | } 84 | """ 85 | } 86 | 87 | case let .argumentList(args) where (1...2).contains(args.count): 88 | if 89 | let isReadonly = args.first(where: \.label?.text == "readonly"), 90 | Expr(isReadonly.expression).asBooleanLiteral?.value == true 91 | { 92 | if binding.initialValue == nil { 93 | return context.diagnose( 94 | .requiresInitialValueForReadonly(binding._syntax), 95 | return: [] 96 | ) 97 | } 98 | 99 | if let didSetHandler { 100 | return context.diagnose( 101 | .unexpectedDidSetAccessor(didSetHandler.accessorSpecifier), 102 | return: [] 103 | ) 104 | } 105 | 106 | if let willSetHandler { 107 | return context.diagnose( 108 | .unexpectedWillSetAccessor(willSetHandler.accessorSpecifier), 109 | return: [] 110 | ) 111 | } 112 | } else if let threadSafety = args.first(where: \.label?.text == "threadSafety") { 113 | setAssociatedObjectFunc = CodeBlockSyntax { 114 | """ 115 | do { 116 | _setAssociatedObject( 117 | newValue, 118 | to: self, 119 | forKey: #function, 120 | threadSafety: \(raw: threadSafety.expression.description) 121 | ) 122 | } 123 | """ 124 | } 125 | } else if let policy = args.first(where: \.label?.text == "policy") { 126 | setAssociatedObjectFunc = CodeBlockSyntax { 127 | """ 128 | do { 129 | _setAssociatedObject( 130 | newValue, 131 | to: self, 132 | forKey: #function, 133 | policy: \(raw: policy.expression.description) 134 | ) 135 | } 136 | """ 137 | } 138 | } else { 139 | return context.diagnose(.unexpectedArguments(decl._syntax), return: []) 140 | } 141 | 142 | default: 143 | return context.diagnose(.unexpectedArguments(decl._syntax), return: []) 144 | } 145 | 146 | if binding.initialValue == nil { 147 | guard case .optional = type else { 148 | return context.diagnose( 149 | .requiresInitialValueForNonOptionals(binding._syntax), 150 | return: [] 151 | ) 152 | } 153 | } 154 | 155 | let getter: CodeBlockSyntax = if let initialValue = binding.initialValue { 156 | CodeBlockSyntax { 157 | """ 158 | return _getAssociatedObject( 159 | forKey: #function, 160 | from: self 161 | ) ?? { 162 | let initialValue: \(raw: type.description) = \(raw: initialValue._syntax.trimmed.description) 163 | _setAssociatedObject( 164 | initialValue, 165 | to: self, 166 | forKey: #function 167 | ) 168 | return self.\(raw: name) 169 | }() 170 | """ 171 | } 172 | } else { 173 | CodeBlockSyntax { 174 | """ 175 | return _getAssociatedObject( 176 | forKey: #function, 177 | from: self 178 | ) 179 | """ 180 | } 181 | } 182 | 183 | if let setAssociatedObjectFunc { 184 | let oldValueID = context.makeUniqueName(name) 185 | let isOldValueNeeded: Bool = { 186 | guard 187 | let didSetHandler, 188 | let body = didSetHandler.body 189 | else { return false } 190 | 191 | let parameter = didSetHandler.parameters?.name ?? "oldValue" 192 | return body.description.range(of: parameter.description) != nil 193 | }() 194 | 195 | let setter = CodeBlockSyntax { 196 | if isOldValueNeeded { 197 | """ 198 | let \(oldValueID) = \(raw: name) 199 | """ 200 | } 201 | 202 | if let willSetHandler { 203 | DoStmtSyntax { 204 | if let param = willSetHandler.parameters?.name { 205 | """ 206 | let \(param) = newValue 207 | """ 208 | } 209 | if let body = willSetHandler.body { 210 | body.statements.trimmed 211 | } 212 | } 213 | } 214 | 215 | setAssociatedObjectFunc.statements 216 | 217 | if let didSetHandler { 218 | // Compiler produces a warning when parameter is unused 219 | DoStmtSyntax { 220 | if isOldValueNeeded { 221 | """ 222 | let \(didSetHandler.parameters?.name ?? "oldValue") = \(oldValueID) 223 | """ 224 | } 225 | if let body = didSetHandler.body { 226 | body.statements.trimmed 227 | } 228 | } 229 | } 230 | } 231 | 232 | return [ 233 | AccessorDeclSyntax( 234 | accessorSpecifier: .keyword(.get), 235 | body: getter 236 | ), 237 | AccessorDeclSyntax( 238 | accessorSpecifier: .keyword(.set), 239 | body: setter 240 | ), 241 | ] 242 | } else { 243 | return [ 244 | AccessorDeclSyntax( 245 | accessorSpecifier: .keyword(.get), 246 | body: getter 247 | ), 248 | ] 249 | } 250 | } 251 | } 252 | 253 | fileprivate extension Diagnostic { 254 | static func requiresComputedProperty(_ node: some SyntaxProtocol) -> Self { 255 | DiagnosticBuilder(for: node) 256 | .messageID(domain: "AssociatedObject", id: "requeres_computed_property") 257 | .message("`@AssociatedObject` must be attached to a computed property declaration.") 258 | .build() 259 | } 260 | 261 | static func requiresExplicitType(_ node: some SyntaxProtocol) -> Self { 262 | DiagnosticBuilder(for: node) 263 | .messageID(domain: "AssociatedObject", id: "requres_explicit_type") 264 | .message("`@AssociatedObject` requires explicit type declaration.") 265 | .build() 266 | } 267 | 268 | static func requiresInitialValueForNonOptionals(_ node: some SyntaxProtocol) -> Self { 269 | DiagnosticBuilder(for: node) 270 | .messageID(domain: "AssociatedObject", id: "requres_initial_value_for_non-optionals") 271 | .message("`@AssociatedObject` requires initial value for non-optional types.") 272 | .build() 273 | } 274 | 275 | static func requiresInitialValueForReadonly(_ node: some SyntaxProtocol) -> Self { 276 | DiagnosticBuilder(for: node) 277 | .messageID(domain: "AssociatedObject", id: "requres_initial_value_for_non-optionals") 278 | .message("`@AssociatedObject` requires initial value for readonly properties.") 279 | .build() 280 | } 281 | 282 | static func unexpectedGetAccessor(_ node: some SyntaxProtocol) -> Self { 283 | DiagnosticBuilder(for: node) 284 | .messageID(domain: "AssociatedObject", id: "unexpected_get_accessor") 285 | .message("`@AssociatedObject` does not support custom `get` accessors") 286 | .build() 287 | } 288 | 289 | static func unexpectedDidSetAccessor(_ node: some SyntaxProtocol) -> Self { 290 | DiagnosticBuilder(for: node) 291 | .messageID(domain: "AssociatedObject", id: "unexpected_get_accessor") 292 | .message("Readonly `@AssociatedObject` does not support `didSet` accessors") 293 | .build() 294 | } 295 | 296 | static func unexpectedWillSetAccessor(_ node: some SyntaxProtocol) -> Self { 297 | DiagnosticBuilder(for: node) 298 | .messageID(domain: "AssociatedObject", id: "unexpected_get_accessor") 299 | .message("Readonly `@AssociatedObject` does not support `willSet` accessors") 300 | .build() 301 | } 302 | 303 | static func unexpectedSetAccessor(_ node: some SyntaxProtocol) -> Self { 304 | DiagnosticBuilder(for: node) 305 | .messageID(domain: "AssociatedObject", id: "unexpected_set_accessor") 306 | .message("`@AssociatedObject` does not support custom `set` accessors") 307 | .suggestReplacement( 308 | "Use `didSet` instead", 309 | old: node, 310 | new: TokenSyntax.keyword(.didSet) 311 | ) 312 | .build() 313 | } 314 | 315 | static func unexpectedArguments(_ node: VariableDeclSyntax) -> Self { 316 | return DiagnosticBuilder(for: node) 317 | .messageID(domain: "AssociatedObject", id: "unexpected_number_of_args") 318 | .message( 319 | """ 320 | [internal] `@AssociatedObject` received unexpected args, submit an issue here: \ 321 | https://github.com/capturecontext/swift-foundation-extensions 322 | """ 323 | ) 324 | .suggestReplacement( 325 | "Remove arguments", 326 | old: node, 327 | new: { 328 | var suggestion = node.detached 329 | suggestion.attributes = .init(suggestion.attributes.map { attribute in 330 | guard 331 | case var .attribute(attribute) = attribute, 332 | attribute.attributeName.description == "AssociatedObject" 333 | else { return attribute } 334 | 335 | attribute.arguments = nil 336 | attribute.leftParen = nil 337 | attribute.rightParen = nil 338 | 339 | return .attribute(attribute) 340 | }) 341 | return suggestion 342 | }() 343 | ) 344 | .suggestReplacement( 345 | "Replace arguments", 346 | old: node, 347 | new: { 348 | var suggestion = node.detached 349 | suggestion.attributes = .init(suggestion.attributes.map { attribute in 350 | guard 351 | case var .attribute(attribute) = attribute, 352 | attribute.attributeName.description == "AssociatedObject" 353 | else { return attribute } 354 | 355 | attribute.arguments = .argumentList(.init { 356 | LabeledExprSyntax( 357 | expression: ExprSyntax(stringLiteral: "<#AssociationPolicy#>") 358 | ) 359 | LabeledExprSyntax( 360 | label: "readonly", 361 | expression: ExprSyntax(stringLiteral: "<#Bool#>") 362 | ) 363 | }) 364 | 365 | return .attribute(attribute) 366 | }) 367 | return suggestion 368 | }() 369 | ) 370 | .build() 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /Sources/FoundationExtensionsMacrosPlugin/Helpers/Diagnostics+.swift: -------------------------------------------------------------------------------- 1 | import SwiftDiagnostics 2 | import SwiftSyntaxMacros 3 | 4 | extension MacroExpansionContext { 5 | func diagnose(_ diagnostic: Diagnostic, return value: T) -> T { 6 | self.diagnose(diagnostic) 7 | return value 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/FoundationExtensionsMacrosPlugin/Helpers/Operators.swift: -------------------------------------------------------------------------------- 1 | func ==( 2 | lhs: KeyPath, 3 | rhs: B 4 | ) -> (A) -> Bool { 5 | return { a in 6 | a[keyPath: lhs] == rhs 7 | } 8 | } 9 | 10 | func !=( 11 | lhs: KeyPath, 12 | rhs: B 13 | ) -> (A) -> Bool { 14 | return { a in 15 | a[keyPath: lhs] != rhs 16 | } 17 | } 18 | 19 | func ||( 20 | lhs: @escaping (A) -> Bool, 21 | rhs: @escaping (A) -> Bool 22 | ) -> (A) -> Bool { 23 | return { a in 24 | lhs(a) || rhs(a) 25 | } 26 | } 27 | 28 | func &&( 29 | lhs: @escaping (A) -> Bool, 30 | rhs: @escaping (A) -> Bool 31 | ) -> (A) -> Bool { 32 | return { a in 33 | lhs(a) && rhs(a) 34 | } 35 | } 36 | 37 | prefix func !( 38 | f: @escaping (A) -> Bool 39 | ) -> (A) -> Bool { 40 | return { a in 41 | !f(a) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/FoundationExtensionsMacrosPlugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | struct FoundationExtensionsPlugin: CompilerPlugin { 6 | let providingMacros: [Macro.Type] = [ 7 | AssociatedObjectMacro.self 8 | ] 9 | } 10 | 11 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsMacrosPluginTests/AssociatedObjectTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MacroTesting 3 | import FoundationExtensionsMacrosPlugin 4 | 5 | final class AssociatedObjectTests: XCTestCase { 6 | override func invokeTest() { 7 | withMacroTesting( 8 | isRecording: false, 9 | macros: [ 10 | "AssociatedObject": AssociatedObjectMacro.self 11 | ] 12 | ) { 13 | super.invokeTest() 14 | } 15 | } 16 | 17 | func testAttachmentToImmutableProperty() { 18 | assertMacro { 19 | """ 20 | extension Object { 21 | @AssociatedObject 22 | let value: Int = 0 23 | } 24 | """ 25 | } diagnostics: { 26 | """ 27 | extension Object { 28 | @AssociatedObject 29 | let value: Int = 0 30 | ╰─ 🛑 `@AssociatedObject` must be attached to a computed property declaration. 31 | } 32 | """ 33 | } 34 | } 35 | 36 | func testAttachmentToVariableWithDefaultValue_ImplicitType() { 37 | assertMacro { 38 | """ 39 | extension Object { 40 | @AssociatedObject 41 | var value = 0 42 | } 43 | """ 44 | } diagnostics: { 45 | """ 46 | extension Object { 47 | @AssociatedObject 48 | var value = 0 49 | ╰─ 🛑 `@AssociatedObject` requires explicit type declaration. 50 | } 51 | """ 52 | } 53 | } 54 | 55 | func testAttachmentToStoredVariable_NoInitialValue() { 56 | assertMacro { 57 | """ 58 | extension Object { 59 | @AssociatedObject 60 | var value: Int 61 | } 62 | """ 63 | } diagnostics: { 64 | """ 65 | extension Object { 66 | @AssociatedObject 67 | var value: Int 68 | ╰─ 🛑 `@AssociatedObject` requires initial value for non-optional types. 69 | } 70 | """ 71 | } 72 | } 73 | 74 | func testAttachmentToVariableWithDefaultValue() { 75 | assertMacro { 76 | """ 77 | extension Object { 78 | @AssociatedObject 79 | var value: Int = 0 80 | } 81 | """ 82 | } expansion: { 83 | """ 84 | extension Object { 85 | var value: Int = 0 { 86 | get { 87 | return _getAssociatedObject( 88 | forKey: #function, 89 | from: self 90 | ) ?? { 91 | let initialValue: Int = 0 92 | _setAssociatedObject( 93 | initialValue, 94 | to: self, 95 | forKey: #function 96 | ) 97 | return self.value 98 | }() 99 | } 100 | set { 101 | do { 102 | _setAssociatedObject( 103 | newValue, 104 | to: self, 105 | forKey: #function 106 | ) 107 | } 108 | } 109 | } 110 | } 111 | """ 112 | } 113 | } 114 | 115 | func testAttachmentToOptionalVariable() { 116 | assertMacro { 117 | """ 118 | extension Object { 119 | @AssociatedObject 120 | var value: Int? 121 | } 122 | """ 123 | } expansion: { 124 | """ 125 | extension Object { 126 | var value: Int? { 127 | get { 128 | return _getAssociatedObject( 129 | forKey: #function, 130 | from: self 131 | ) 132 | } 133 | set { 134 | do { 135 | _setAssociatedObject( 136 | newValue, 137 | to: self, 138 | forKey: #function 139 | ) 140 | } 141 | } 142 | } 143 | } 144 | """ 145 | } 146 | } 147 | 148 | // 🛑 Macro is not able to process unrelated params [reason: lack of type info] 149 | // ⚠️ Params handling is responsibility of API providing library 150 | func testCustomParams_UnrelatedParams() { 151 | assertMacro { 152 | """ 153 | extension Object { 154 | @AssociatedObject("Hello, World") 155 | var value: Int? 156 | } 157 | """ 158 | } diagnostics: { 159 | """ 160 | extension Object { 161 | @AssociatedObject("Hello, World") 162 | ╰─ 🛑 [internal] `@AssociatedObject` received unexpected args, submit an issue here: https://github.com/capturecontext/swift-foundation-extensions 163 | ✏️ Remove arguments 164 | ✏️ Replace arguments 165 | var value: Int? 166 | } 167 | """ 168 | }fixes: { 169 | """ 170 | extension Object { 171 | @AssociatedObject 172 | var value: Int? 173 | } 174 | """ 175 | } expansion: { 176 | """ 177 | extension Object { 178 | var value: Int? { 179 | get { 180 | return _getAssociatedObject( 181 | forKey: #function, 182 | from: self 183 | ) 184 | } 185 | set { 186 | do { 187 | _setAssociatedObject( 188 | newValue, 189 | to: self, 190 | forKey: #function 191 | ) 192 | } 193 | } 194 | } 195 | } 196 | """ 197 | } 198 | } 199 | 200 | // ⚠️ Macro is not able to fully process unexpected number of args 201 | // ⚠️ Params handling is responsibility of API providing library 202 | func testCustomParams_MultipleParams() { 203 | assertMacro { 204 | """ 205 | extension Object { 206 | @AssociatedObject(.copy, .nonatomic, .readonly) 207 | var value: Int? 208 | } 209 | """ 210 | } diagnostics: { 211 | """ 212 | extension Object { 213 | @AssociatedObject(.copy, .nonatomic, .readonly) 214 | ╰─ 🛑 [internal] `@AssociatedObject` received unexpected args, submit an issue here: https://github.com/capturecontext/swift-foundation-extensions 215 | ✏️ Remove arguments 216 | ✏️ Replace arguments 217 | var value: Int? 218 | } 219 | """ 220 | }fixes: { 221 | """ 222 | extension Object { 223 | @AssociatedObject 224 | var value: Int? 225 | } 226 | """ 227 | } expansion: { 228 | """ 229 | extension Object { 230 | var value: Int? { 231 | get { 232 | return _getAssociatedObject( 233 | forKey: #function, 234 | from: self 235 | ) 236 | } 237 | set { 238 | do { 239 | _setAssociatedObject( 240 | newValue, 241 | to: self, 242 | forKey: #function 243 | ) 244 | } 245 | } 246 | } 247 | } 248 | """ 249 | } 250 | } 251 | 252 | func testCustomParams_AssociationPolicy() { 253 | assertMacro { 254 | """ 255 | extension Object { 256 | @AssociatedObject(policy: .copy(.nonatomic)) 257 | var value: Int? 258 | } 259 | """ 260 | } expansion: { 261 | """ 262 | extension Object { 263 | var value: Int? { 264 | get { 265 | return _getAssociatedObject( 266 | forKey: #function, 267 | from: self 268 | ) 269 | } 270 | set { 271 | do { 272 | _setAssociatedObject( 273 | newValue, 274 | to: self, 275 | forKey: #function, 276 | policy: .copy(.nonatomic) 277 | ) 278 | } 279 | } 280 | } 281 | } 282 | """ 283 | } 284 | } 285 | 286 | func testCustomParams_ThreadSafety() { 287 | assertMacro { 288 | """ 289 | extension Object { 290 | @AssociatedObject(threadSafety: .atomic) 291 | var value: Int? 292 | } 293 | """ 294 | } expansion: { 295 | """ 296 | extension Object { 297 | var value: Int? { 298 | get { 299 | return _getAssociatedObject( 300 | forKey: #function, 301 | from: self 302 | ) 303 | } 304 | set { 305 | do { 306 | _setAssociatedObject( 307 | newValue, 308 | to: self, 309 | forKey: #function, 310 | threadSafety: .atomic 311 | ) 312 | } 313 | } 314 | } 315 | } 316 | """ 317 | } 318 | } 319 | 320 | func testComputedValue_CustomSetAccessor() { 321 | assertMacro { 322 | """ 323 | extension Object { 324 | @AssociatedObject 325 | var value: Int? { 326 | set { print(newValue) } 327 | } 328 | } 329 | """ 330 | } diagnostics: { 331 | """ 332 | extension Object { 333 | @AssociatedObject 334 | var value: Int? { 335 | ╰─ 🛑 `@AssociatedObject` does not support custom `set` accessors 336 | ✏️ Use `didSet` instead 337 | set { print(newValue) } 338 | } 339 | } 340 | """ 341 | }fixes: { 342 | """ 343 | extension Object { 344 | @AssociatedObject 345 | var value: Int? { 346 | set { print(newValue) } 347 | } 348 | } 349 | """ 350 | } 351 | } 352 | 353 | func testComputedValue_CustomWillSetDidSetAccessors() { 354 | assertMacro { 355 | """ 356 | extension Object { 357 | @AssociatedObject 358 | var value: Int? { 359 | willSet { print(newValue) } 360 | didSet { print(oldValue) } 361 | } 362 | } 363 | """ 364 | } expansion: { 365 | """ 366 | extension Object { 367 | var value: Int? { 368 | willSet { print(newValue) } 369 | didSet { print(oldValue) } 370 | get { 371 | return _getAssociatedObject( 372 | forKey: #function, 373 | from: self 374 | ) 375 | } 376 | 377 | set { 378 | let __macro_local_5valuefMu_ = value 379 | do { 380 | print(newValue) 381 | } 382 | do { 383 | _setAssociatedObject( 384 | newValue, 385 | to: self, 386 | forKey: #function 387 | ) 388 | } 389 | do { 390 | let oldValue = __macro_local_5valuefMu_ 391 | print(oldValue) 392 | } 393 | } 394 | } 395 | } 396 | """ 397 | } 398 | } 399 | 400 | func testComputedValue_CustomWillSetDidSetAccessors_CustomArgs() { 401 | assertMacro { 402 | """ 403 | extension Object { 404 | @AssociatedObject 405 | var value: Int? { 406 | willSet(new) { print(new) } 407 | didSet(old) { print(old) } 408 | } 409 | } 410 | """ 411 | } expansion: { 412 | """ 413 | extension Object { 414 | var value: Int? { 415 | willSet(new) { print(new) } 416 | didSet(old) { print(old) } 417 | get { 418 | return _getAssociatedObject( 419 | forKey: #function, 420 | from: self 421 | ) 422 | } 423 | 424 | set { 425 | let __macro_local_5valuefMu_ = value 426 | do { 427 | let new = newValue 428 | print(new) 429 | } 430 | do { 431 | _setAssociatedObject( 432 | newValue, 433 | to: self, 434 | forKey: #function 435 | ) 436 | } 437 | do { 438 | let old = __macro_local_5valuefMu_ 439 | print(old) 440 | } 441 | } 442 | } 443 | } 444 | """ 445 | } 446 | } 447 | 448 | func testStoredValue_CustomWillSetDidSetAccessors() { 449 | assertMacro { 450 | """ 451 | extension Object { 452 | @AssociatedObject 453 | var value: Int = 0 { 454 | willSet { print(newValue) } 455 | didSet { print(oldValue) } 456 | } 457 | } 458 | """ 459 | } expansion: { 460 | """ 461 | extension Object { 462 | var value: Int = 0 { 463 | willSet { print(newValue) } 464 | didSet { print(oldValue) } 465 | get { 466 | return _getAssociatedObject( 467 | forKey: #function, 468 | from: self 469 | ) ?? { 470 | let initialValue: Int = 0 471 | _setAssociatedObject( 472 | initialValue, 473 | to: self, 474 | forKey: #function 475 | ) 476 | return self.value 477 | }() 478 | } 479 | 480 | set { 481 | let __macro_local_5valuefMu_ = value 482 | do { 483 | print(newValue) 484 | } 485 | do { 486 | _setAssociatedObject( 487 | newValue, 488 | to: self, 489 | forKey: #function 490 | ) 491 | } 492 | do { 493 | let oldValue = __macro_local_5valuefMu_ 494 | print(oldValue) 495 | } 496 | } 497 | } 498 | } 499 | """ 500 | } 501 | } 502 | 503 | func testStoredValue_CustomWillSetDidSetAccessors_CustomArgs() { 504 | assertMacro { 505 | """ 506 | extension Object { 507 | @AssociatedObject 508 | var value: Int = 0 { 509 | willSet(new) { print(new) } 510 | didSet(old) { print(old) } 511 | } 512 | } 513 | """ 514 | } expansion: { 515 | """ 516 | extension Object { 517 | var value: Int = 0 { 518 | willSet(new) { print(new) } 519 | didSet(old) { print(old) } 520 | get { 521 | return _getAssociatedObject( 522 | forKey: #function, 523 | from: self 524 | ) ?? { 525 | let initialValue: Int = 0 526 | _setAssociatedObject( 527 | initialValue, 528 | to: self, 529 | forKey: #function 530 | ) 531 | return self.value 532 | }() 533 | } 534 | 535 | set { 536 | let __macro_local_5valuefMu_ = value 537 | do { 538 | let new = newValue 539 | print(new) 540 | } 541 | do { 542 | _setAssociatedObject( 543 | newValue, 544 | to: self, 545 | forKey: #function 546 | ) 547 | } 548 | do { 549 | let old = __macro_local_5valuefMu_ 550 | print(old) 551 | } 552 | } 553 | } 554 | } 555 | """ 556 | } 557 | } 558 | 559 | func testStoredValue_CustomDidSetAccessor_UnusedOldValue() { 560 | assertMacro { 561 | """ 562 | extension Object { 563 | @AssociatedObject 564 | var value: Int = 0 { 565 | didSet { print("Hello") } 566 | } 567 | } 568 | """ 569 | } expansion: { 570 | """ 571 | extension Object { 572 | var value: Int = 0 { 573 | didSet { print("Hello") } 574 | get { 575 | return _getAssociatedObject( 576 | forKey: #function, 577 | from: self 578 | ) ?? { 579 | let initialValue: Int = 0 580 | _setAssociatedObject( 581 | initialValue, 582 | to: self, 583 | forKey: #function 584 | ) 585 | return self.value 586 | }() 587 | } 588 | 589 | set { 590 | do { 591 | _setAssociatedObject( 592 | newValue, 593 | to: self, 594 | forKey: #function 595 | ) 596 | } 597 | do { 598 | print("Hello") 599 | } 600 | } 601 | } 602 | } 603 | """ 604 | } 605 | } 606 | 607 | func testReadonlyValue() { 608 | assertMacro { 609 | """ 610 | extension Object { 611 | @AssociatedObject(readonly: true) 612 | var value: Int = 0 613 | } 614 | """ 615 | } expansion: { 616 | """ 617 | extension Object { 618 | var value: Int = 0 { 619 | get { 620 | return _getAssociatedObject( 621 | forKey: #function, 622 | from: self 623 | ) ?? { 624 | let initialValue: Int = 0 625 | _setAssociatedObject( 626 | initialValue, 627 | to: self, 628 | forKey: #function 629 | ) 630 | return self.value 631 | }() 632 | } 633 | } 634 | } 635 | """ 636 | } 637 | } 638 | 639 | func testReadonlyValue_NoInitialValue() { 640 | assertMacro { 641 | """ 642 | extension Object { 643 | @AssociatedObject(readonly: true) 644 | var value: Int 645 | } 646 | """ 647 | } diagnostics: { 648 | """ 649 | extension Object { 650 | @AssociatedObject(readonly: true) 651 | var value: Int 652 | ╰─ 🛑 `@AssociatedObject` requires initial value for readonly properties. 653 | } 654 | """ 655 | } 656 | } 657 | 658 | func testReadonlyValue_CustomDidSet() { 659 | assertMacro { 660 | """ 661 | extension Object { 662 | @AssociatedObject(readonly: true) 663 | var value: Int = 0 { 664 | didSet { print(value) } 665 | } 666 | } 667 | """ 668 | } diagnostics: { 669 | """ 670 | extension Object { 671 | @AssociatedObject(readonly: true) 672 | var value: Int = 0 { 673 | ╰─ 🛑 Readonly `@AssociatedObject` does not support `didSet` accessors 674 | didSet { print(value) } 675 | } 676 | } 677 | """ 678 | } 679 | } 680 | 681 | func testReadonlyValue_CustomWillSet() { 682 | assertMacro { 683 | """ 684 | extension Object { 685 | @AssociatedObject(readonly: true) 686 | var value: Int = 0 { 687 | didSet { print(value) } 688 | } 689 | } 690 | """ 691 | } diagnostics: { 692 | """ 693 | extension Object { 694 | @AssociatedObject(readonly: true) 695 | var value: Int = 0 { 696 | ╰─ 🛑 Readonly `@AssociatedObject` does not support `didSet` accessors 697 | didSet { print(value) } 698 | } 699 | } 700 | """ 701 | } 702 | } 703 | } 704 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsMacrosTests/AssociatedObjectTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FoundationExtensionsMacros 3 | 4 | class Object {} 5 | extension Object { 6 | @AssociatedObject 7 | var didSetStoredValue: ((Int?) -> Void)? 8 | 9 | @AssociatedObject 10 | var willSetStoredValue: ((Int?) -> Void)? 11 | 12 | @AssociatedObject(readonly: true) 13 | var box: Box = .init(0) 14 | 15 | @AssociatedObject 16 | var storedValue: Int = 0 { 17 | willSet { willSetStoredValue?(newValue) } 18 | didSet { didSetStoredValue?(storedValue) } 19 | } 20 | } 21 | 22 | final class AssociatedObjectTests: XCTestCase { 23 | func testMain() { 24 | let object = Object() 25 | 26 | var value = ( 27 | initial: object.storedValue, 28 | trackedWillSet: Int?.some(0), 29 | trackedWillSetCalls: 0, 30 | trackedDidSet: Int?.some(0), 31 | trackedDidSetCalls: 0 32 | ) 33 | 34 | object.willSetStoredValue = { [weak object] in 35 | value.trackedWillSet = $0 36 | XCTAssertEqual(object?.storedValue, value.trackedDidSet) 37 | 38 | value.trackedWillSetCalls += 1 39 | XCTAssertEqual(value.trackedDidSetCalls, value.trackedWillSetCalls - 1) 40 | } 41 | object.didSetStoredValue = { 42 | XCTAssertEqual(value.trackedWillSet, $0) 43 | value.trackedDidSet = $0 44 | value.trackedDidSetCalls += 1 45 | XCTAssertEqual(value.trackedDidSetCalls, value.trackedWillSetCalls) 46 | } 47 | 48 | XCTAssertEqual(object.storedValue, 0) 49 | 50 | object.storedValue = 1 51 | XCTAssertEqual(object.storedValue, 1) 52 | XCTAssertEqual(value.trackedDidSetCalls, 1) 53 | 54 | object.storedValue = 69 55 | XCTAssertEqual(object.storedValue, 69) 56 | XCTAssertEqual(value.trackedDidSetCalls, 2) 57 | 58 | object.storedValue = 0 59 | XCTAssertEqual(object.storedValue, 0) 60 | XCTAssertEqual(value.trackedDidSetCalls, 3) 61 | 62 | // Readonly 63 | // object.box = .init(2) 64 | 65 | XCTAssertEqual(object.box.content, 0) 66 | object.box.content += 1 67 | XCTAssertEqual(object.box.content, 1) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsTests/AssociatingObjectTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FoundationExtensions 3 | 4 | final class AssociatingObjectTests: XCTestCase { 5 | func testMain() { 6 | struct Struct: Equatable { 7 | var value: Int 8 | } 9 | class CustomObject {} 10 | class CustomNSObject: NSObject {} 11 | class CustomAssociatingObject: AssociatingObject { 12 | var _struct: Struct { 13 | get { getAssociatedObject(forKey: #function).or(.init(value: 0)) } 14 | set { setAssociatedObject(newValue, forKey: #function)} 15 | } 16 | } 17 | 18 | let nsObject = NSObject() 19 | let customObject = CustomObject() 20 | let customNSObject = CustomNSObject() 21 | let customAssociatingObject = CustomAssociatingObject() 22 | 23 | // simple check for struct storage 24 | XCTAssertEqual(customAssociatingObject._struct, .init(value: 0)) 25 | 26 | customAssociatingObject._struct.value = 1 27 | XCTAssertEqual(customAssociatingObject._struct, .init(value: 1)) 28 | 29 | nsObject.setAssociatedObject(91, forKey: "value") 30 | customNSObject.setAssociatedObject(92, forKey: "value") 31 | customAssociatingObject.setAssociatedObject(93, forKey: "value") 32 | _setAssociatedObject(94, to: customObject, forKey: "value") 33 | 34 | // check if set overrides values on different objects 35 | 36 | nsObject.setAssociatedObject(91, forKey: "value") 37 | customNSObject.setAssociatedObject(92, forKey: "value") 38 | customAssociatingObject.setAssociatedObject(93, forKey: "value") 39 | _setAssociatedObject(94, to: customObject, forKey: "value") 40 | 41 | XCTAssertEqual(91, nsObject.getAssociatedObject(forKey: "value")) 42 | XCTAssertEqual(92, customNSObject.getAssociatedObject(forKey: "value")) 43 | XCTAssertEqual(93, customAssociatingObject.getAssociatedObject(forKey: "value")) 44 | XCTAssertEqual(94, _getAssociatedObject(forKey: "value", from: customObject)) 45 | 46 | // check if prev value is removed if the new one of other type is set 47 | 48 | nsObject.setAssociatedObject("test", forKey: "value") 49 | XCTAssertEqual("test", nsObject.getAssociatedObject(forKey: "value")) 50 | XCTAssertEqual(Int?.none, nsObject.getAssociatedObject(forKey: "value")) 51 | 52 | // test set and get in different independent functions 53 | 54 | nsObject.setAssociatedObject("0", forKey: "zero") 55 | XCTAssertEqual("0", getValueForKey_zero(from: nsObject)) 56 | 57 | setZeroForKey_zero(to: nsObject) 58 | XCTAssertEqual(0, getValueForKey_zero(from: nsObject)) 59 | XCTAssert(getValueForKey_zero(of: Any.self, from: customObject).isNil) 60 | } 61 | 62 | func testStaticStringAddress() { 63 | let a: StaticString = #function 64 | let b: StaticString = #function 65 | let c: StaticString = "testStaticStringAddress()" 66 | let d: StaticString = "testStaticStringAddress()" 67 | let f: StaticString = "other" 68 | let g: StaticString = "testStaticStringAddress_same_prefix" 69 | 70 | func getBaseAddress(for key: StaticString) -> String? { 71 | key.utf8Start.debugDescription 72 | } 73 | 74 | XCTAssertEqual(a.description, b.description) 75 | XCTAssertEqual(getBaseAddress(for: a), getBaseAddress(for: b)) 76 | 77 | XCTAssertEqual(c.description, d.description) 78 | XCTAssertEqual(getBaseAddress(for: c), getBaseAddress(for: d)) 79 | 80 | XCTAssertEqual(a.description, c.description) 81 | XCTAssertEqual(getBaseAddress(for: a), getBaseAddress(for: c)) 82 | 83 | XCTAssertNotEqual(a.description, f.description) 84 | XCTAssertNotEqual(getBaseAddress(for: a), getBaseAddress(for: f)) 85 | 86 | XCTAssertNotEqual(a.description, g.description) 87 | XCTAssertNotEqual(getBaseAddress(for: a), getBaseAddress(for: g)) 88 | } 89 | } 90 | 91 | fileprivate func setZeroForKey_zero(to object: AnyObject) { 92 | _setAssociatedObject(0, to: object, forKey: "zero") 93 | } 94 | 95 | fileprivate func getValueForKey_zero( 96 | of type: T.Type = T.self, 97 | from object: AnyObject 98 | ) -> T? { 99 | _getAssociatedObject(forKey: "zero", from: object) 100 | } 101 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsTests/CodingKeysTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FoundationExtensions 3 | 4 | final class CodingKeysTests: XCTestCase { 5 | struct Object: Codable, Equatable { 6 | init( 7 | optionalValue: Int? = nil, 8 | integerValue: Int = 0, 9 | stringValue: String = "", 10 | boolValue: Bool = false 11 | ) { 12 | self.optionalValue = optionalValue 13 | self.integerValue = integerValue 14 | self.stringValue = stringValue 15 | self.boolValue = boolValue 16 | } 17 | 18 | var optionalValue: Int? = nil 19 | var integerValue: Int = 0 20 | var stringValue: String = "" 21 | var boolValue: Bool = false 22 | 23 | init(from decoder: Decoder) throws { 24 | self = try decoder.decode { container in 25 | return .init( 26 | optionalValue: try container.decodeIfPresent("optionalValue"), 27 | integerValue: try container.decode("integerValue"), 28 | stringValue: try container.decode("stringValue"), 29 | boolValue: try container.decode("boolValue") 30 | ) 31 | } 32 | } 33 | 34 | func encode(to encoder: Encoder) throws { 35 | try encoder.encode { container in 36 | try container.encodeIfPresent(optionalValue, forKey: "optionalValue") 37 | try container.encode(integerValue, forKey: "integerValue") 38 | try container.encode(stringValue, forKey: "stringValue") 39 | try container.encode(boolValue, forKey: "boolValue") 40 | } 41 | } 42 | } 43 | 44 | struct CodableObject: Codable, Equatable { 45 | init( 46 | optionalValue: Int? = nil, 47 | integerValue: Int = 0, 48 | stringValue: String = "", 49 | boolValue: Bool = false 50 | ) { 51 | self.optionalValue = optionalValue 52 | self.integerValue = integerValue 53 | self.stringValue = stringValue 54 | self.boolValue = boolValue 55 | } 56 | 57 | var optionalValue: Int? = nil 58 | var integerValue: Int = 0 59 | var stringValue: String = "" 60 | var boolValue: Bool = false 61 | } 62 | 63 | struct IntegerValueDecoding: Decodable, Equatable { 64 | init(value: Int) { 65 | self.value = value 66 | } 67 | 68 | var value: Int 69 | 70 | init(from decoder: Decoder) throws { 71 | self.value = try decoder.decode { container in 72 | return try container.decode("integerValue") 73 | } 74 | } 75 | } 76 | 77 | func testMain() throws { 78 | let object = Object( 79 | optionalValue: 1, 80 | integerValue: 2, 81 | stringValue: "test", 82 | boolValue: true 83 | ) 84 | 85 | let encoder = JSONEncoder() 86 | encoder.outputFormatting = [ .prettyPrinted] 87 | if #available(iOS 11.0, tvOS 11.0, *) { 88 | encoder.outputFormatting.insert(.sortedKeys) 89 | } 90 | 91 | let decoder = JSONDecoder() 92 | 93 | let encodedData = try encoder.encode(object) 94 | 95 | if #available(iOS 11.0, tvOS 11.0, *) { 96 | XCTAssertEqual( 97 | try XCTUnwrap(String(data: encodedData, encoding: .utf8)), 98 | """ 99 | { 100 | "boolValue" : true, 101 | "integerValue" : 2, 102 | "optionalValue" : 1, 103 | "stringValue" : "test" 104 | } 105 | """ 106 | ) 107 | } 108 | 109 | XCTAssertEqual( 110 | object, 111 | try decoder.decode(Object.self, from: encodedData) 112 | ) 113 | 114 | XCTAssertEqual( 115 | try decoder.decode(CodableObject.self, from: encodedData), 116 | CodableObject( 117 | optionalValue: 1, 118 | integerValue: 2, 119 | stringValue: "test", 120 | boolValue: true 121 | ) 122 | ) 123 | 124 | XCTAssertEqual( 125 | try decoder.decode(IntegerValueDecoding.self, from: encodedData), 126 | IntegerValueDecoding(value: 2) 127 | ) 128 | 129 | XCTAssertEqual( 130 | try encoder.encode(CodableObject( 131 | optionalValue: 1, 132 | integerValue: 2, 133 | stringValue: "test", 134 | boolValue: true 135 | )), 136 | encodedData 137 | ) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsTests/EquatedTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FoundationExtensions 3 | 4 | final class EquatedTests: XCTestCase { 5 | func testMain() { 6 | struct Const { 7 | var value: () -> Int 8 | 9 | init(_ value: Int) { 10 | self.value = { value } 11 | } 12 | } 13 | 14 | struct State: Equatable { 15 | @Equated(by: .property { $0.value() }) 16 | var const = Const(0) 17 | } 18 | 19 | XCTAssertEqual(State(const: .init(0)), State(const: .init(0))) 20 | XCTAssertNotEqual(State(const: .init(0)), State(const: .init(1))) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsTests/IndirectTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FoundationExtensions 3 | 4 | final class IndirectTests: XCTestCase { 5 | func testMain() { 6 | struct LinkedListNode: Equatable { 7 | var value: Value 8 | @Indirect 9 | var next: LinkedListNode? 10 | } 11 | 12 | @Indirect 13 | var root = LinkedListNode(value: 0) 14 | 15 | @Indirect 16 | var first: LinkedListNode? = LinkedListNode(value: 1) 17 | 18 | @Indirect 19 | var second: LinkedListNode? = LinkedListNode(value: 2) 20 | 21 | root.next = first 22 | root.next?.next = second 23 | 24 | XCTAssertEqual( 25 | root, 26 | LinkedListNode( 27 | value: 0, 28 | next: LinkedListNode( 29 | value: 1, 30 | next: LinkedListNode(value: 2) 31 | ) 32 | ) 33 | ) 34 | 35 | // Equated by value 36 | XCTAssertEqual( 37 | first, 38 | LinkedListNode(value: 1) 39 | ) 40 | 41 | // CoW 42 | var third = $second 43 | XCTAssert(third._sharesStorage(with: $second) == true) 44 | 45 | third._setValue(LinkedListNode(value: 2)) 46 | XCTAssertEqual(second?.value, 2) 47 | XCTAssert(third._sharesStorage(with: $second) == true) 48 | 49 | third._modifyValue { $0?.value += 1 } 50 | XCTAssertEqual(second?.value, 3) 51 | XCTAssert(third._sharesStorage(with: $second) == true) 52 | 53 | third.wrappedValue?.value = 4 54 | XCTAssertEqual(second?.value, 3) 55 | XCTAssertEqual(third.wrappedValue?.value, 4) 56 | XCTAssert(third._sharesStorage(with: $second) == false) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsTests/ObjectProxyTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FoundationExtensions 3 | 4 | final class ObjectProxyTests: XCTestCase { 5 | func testMain() { 6 | class Object { 7 | class WrappedObject { 8 | var value: Int = 0 9 | } 10 | 11 | internal let wrapped: WrappedObject = .init() 12 | 13 | @PropertyProxy(\Object.wrapped.value) 14 | var value 15 | } 16 | 17 | let object = Object() 18 | 19 | XCTAssertEqual(object.value, 0) 20 | XCTAssertEqual(object.wrapped.value, 0) 21 | 22 | object.value = 1 23 | XCTAssertEqual(object.value, 1) 24 | XCTAssertEqual(object.wrapped.value, 1) 25 | 26 | object.value += 1 27 | XCTAssertEqual(object.value, 2) 28 | XCTAssertEqual(object.wrapped.value, 2) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsTests/ReferenceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FoundationExtensions 3 | 4 | final class ReferenceTests: XCTestCase { 5 | func testAssociatingObject() { 6 | class Obj: AssociatingObject {} 7 | let obj = Obj() 8 | let value = 1 9 | 10 | class _Obj: AssociatingObject {} 11 | let _obj = _Obj() 12 | let _value = 2 13 | 14 | XCTAssertNil(obj.getAssociatedObject(of: Int.self, forKey: "value")) 15 | XCTAssertTrue(obj.setAssociatedObject(value, forKey: "value")) 16 | XCTAssertEqual(obj.getAssociatedObject(forKey: "value"), value) 17 | 18 | XCTAssertNil(_obj.getAssociatedObject(of: Int.self, forKey: "value")) 19 | XCTAssertTrue(_obj.setAssociatedObject(_value, forKey: "value")) 20 | XCTAssertEqual(_obj.getAssociatedObject(forKey: "value"), _value) 21 | 22 | XCTAssertEqual(obj.getAssociatedObject(forKey: "value"), value) 23 | } 24 | 25 | func testAssociatingNSObject() { 26 | class Obj: NSObject {} 27 | let obj = Obj() 28 | 29 | let value = 1 30 | XCTAssertEqual(obj.getAssociatedObject(of: Int.self, forKey: "value"), nil) 31 | 32 | obj.setAssociatedObject(value, forKey: "value") 33 | XCTAssertEqual(obj.getAssociatedObject(forKey: "value"), value) 34 | } 35 | 36 | func testReferenceObservation() { 37 | class Object: ReferenceProvider { 38 | var value = 0 39 | } 40 | 41 | let object = Object() 42 | let reference = object.reference(for: \.value) 43 | 44 | var handledOnChange: Int? 45 | var numberOfTrackedChanges = 0 46 | var numberOfTrackedSets = 0 47 | let trackedReference = reference.onChange { 48 | handledOnChange = $0 49 | numberOfTrackedChanges += 1 50 | }.onSet { _ in 51 | numberOfTrackedSets += 1 52 | } 53 | 54 | object.value = 1 55 | 56 | XCTAssertEqual(object.value, reference.wrappedValue) 57 | XCTAssertEqual(object.value, trackedReference.wrappedValue) 58 | 59 | // Reference does not handle direct object changes 60 | XCTAssertEqual(handledOnChange, nil) 61 | XCTAssertEqual(numberOfTrackedSets, 0) 62 | XCTAssertEqual(numberOfTrackedChanges, 0) 63 | 64 | trackedReference.wrappedValue = 2 65 | 66 | XCTAssertEqual(object.value, 2) 67 | XCTAssertEqual(object.value, reference.wrappedValue) 68 | XCTAssertEqual(object.value, trackedReference.wrappedValue) 69 | XCTAssertEqual(object.value, handledOnChange) 70 | XCTAssertEqual(numberOfTrackedSets, 1) 71 | XCTAssertEqual(numberOfTrackedChanges, 1) 72 | 73 | trackedReference.wrappedValue = 2 74 | 75 | XCTAssertEqual(object.value, 2) 76 | XCTAssertEqual(object.value, reference.wrappedValue) 77 | XCTAssertEqual(object.value, trackedReference.wrappedValue) 78 | XCTAssertEqual(object.value, handledOnChange) 79 | XCTAssertEqual(numberOfTrackedSets, 2) 80 | XCTAssertEqual(numberOfTrackedChanges, 1) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsTests/ResettableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FoundationExtensions 3 | 4 | func andOf(_ values: Bool...) -> Bool { 5 | values.reduce(true) { $0 && $1 } 6 | } 7 | 8 | final class ResettableTests: XCTestCase { 9 | struct TestStruct: Equatable { 10 | struct Inner: Equatable { 11 | var value: Int = 0 12 | } 13 | var inner: Inner = .init() 14 | var boolean: Bool = false 15 | var int: Int = 0 16 | var optional: Optional = nil 17 | } 18 | 19 | class TestClass: Equatable { 20 | static func == (lhs: TestClass, rhs: TestClass) -> Bool { 21 | andOf( 22 | lhs.inner == rhs.inner, 23 | lhs.boolean == rhs.boolean, 24 | lhs.int == rhs.int, 25 | lhs.optional == rhs.optional 26 | ) 27 | } 28 | 29 | struct Inner: Equatable { 30 | var value: Int = 0 31 | } 32 | 33 | init() {} 34 | 35 | var inner: Inner = .init() 36 | var boolean: Bool = false 37 | var int: Int = 0 38 | var optional: Optional = nil 39 | } 40 | 41 | public func testUndoRedoValueType() { 42 | var value = TestStruct() 43 | let resettable = Resettable(value) 44 | 45 | resettable.inner.value(1) 46 | value.inner.value = 1 47 | 48 | resettable.boolean(true) 49 | value.boolean = true 50 | 51 | resettable.inner.value(2) 52 | value.inner.value = 2 53 | 54 | resettable.int(10) 55 | value.int = 10 56 | 57 | XCTAssertEqual(resettable.wrappedValue, value) 58 | 59 | resettable.undo() 60 | value.int = 0 61 | XCTAssertEqual(resettable.wrappedValue, value) 62 | 63 | resettable.undo() 64 | value.inner.value = 1 65 | XCTAssertEqual(resettable.wrappedValue, value) 66 | 67 | resettable.undo() 68 | value.boolean = false 69 | XCTAssertEqual(resettable.wrappedValue, value) 70 | 71 | resettable.undo() 72 | value.inner.value = 0 73 | XCTAssertEqual(resettable.wrappedValue, value) 74 | 75 | resettable.redo() 76 | value.inner.value = 1 77 | XCTAssertEqual(resettable.wrappedValue, value) 78 | 79 | resettable.int { $0 += 1 } 80 | value.int = 1 81 | XCTAssertEqual(resettable.wrappedValue, value) 82 | 83 | resettable.undo() 84 | value.int = 0 85 | XCTAssertEqual(resettable.wrappedValue, value) 86 | 87 | resettable.redo() 88 | value.int = 1 89 | XCTAssertEqual(resettable.wrappedValue, value) 90 | 91 | resettable.redo() 92 | value.int = 1 93 | XCTAssertEqual(resettable.wrappedValue, value) 94 | 95 | resettable.optional(.init()) 96 | value.optional = .init() 97 | XCTAssertEqual(resettable.wrappedValue, value) 98 | 99 | resettable.optional.value(1) 100 | value.optional?.value = 1 101 | XCTAssertEqual(resettable.wrappedValue, value) 102 | 103 | resettable.optional(nil) 104 | value.optional = nil 105 | XCTAssertEqual(resettable.wrappedValue, value) 106 | } 107 | 108 | public func testUndoRedoReferenceType() { 109 | let value = TestClass() 110 | let resettable = Resettable(value) 111 | 112 | resettable.inner.value(1) 113 | value.inner.value = 1 114 | 115 | resettable.boolean(true) 116 | value.boolean = true 117 | 118 | resettable.inner.value(2) 119 | value.inner.value = 2 120 | 121 | resettable.int(10) 122 | value.int = 10 123 | 124 | XCTAssertEqual(resettable.wrappedValue, value) 125 | 126 | resettable.undo() 127 | value.int = 0 128 | XCTAssertEqual(resettable.wrappedValue, value) 129 | 130 | resettable.undo() 131 | value.inner.value = 1 132 | XCTAssertEqual(resettable.wrappedValue, value) 133 | 134 | resettable.undo() 135 | value.boolean = false 136 | XCTAssertEqual(resettable.wrappedValue, value) 137 | 138 | resettable.undo() 139 | value.inner.value = 0 140 | XCTAssertEqual(resettable.wrappedValue, value) 141 | 142 | resettable.redo() 143 | value.inner.value = 1 144 | XCTAssertEqual(resettable.wrappedValue, value) 145 | 146 | resettable.int { $0 += 1 } 147 | value.int = 1 148 | XCTAssertEqual(resettable.wrappedValue, value) 149 | 150 | resettable.undo() 151 | value.int = 0 152 | XCTAssertEqual(resettable.wrappedValue, value) 153 | 154 | resettable.redo() 155 | value.int = 1 156 | XCTAssertEqual(resettable.wrappedValue, value) 157 | 158 | resettable.redo() 159 | value.int = 1 160 | XCTAssertEqual(resettable.wrappedValue, value) 161 | 162 | resettable.optional(.init()) 163 | value.optional = .init() 164 | XCTAssertEqual(resettable.wrappedValue, value) 165 | 166 | resettable.optional.value(1) 167 | value.optional?.value = 1 168 | XCTAssertEqual(resettable.wrappedValue, value) 169 | 170 | resettable.optional(nil) 171 | value.optional = nil 172 | XCTAssertEqual(resettable.wrappedValue, value) 173 | } 174 | 175 | func testUndoRedoCollection() { 176 | struct Object: Equatable { 177 | let id: UUID = .init() 178 | var value: Int = 0 179 | } 180 | 181 | var first = Object(value: 0) 182 | var second = Object(value: 1) 183 | let resettable = Resettable([first, second]) 184 | 185 | resettable.collection.swapAt(0, 1) 186 | 187 | XCTAssertEqual(resettable.wrappedValue, [second, first]) 188 | 189 | resettable.collection[safe: 0].value(0) 190 | second.value = 0 191 | 192 | XCTAssertEqual(resettable.wrappedValue, [second, first]) 193 | 194 | resettable.undo() 195 | second.value = 1 196 | XCTAssertEqual(resettable.wrappedValue, [second, first]) 197 | 198 | resettable.undo() 199 | XCTAssertEqual(resettable.wrappedValue, [first, second]) 200 | 201 | resettable.redo() 202 | XCTAssertEqual(resettable.wrappedValue, [second, first]) 203 | 204 | let third = Object(value: -1) 205 | resettable 206 | .collection.swapAt(0, 1) 207 | .collection[safe: 0].value(2) 208 | .collection.append(third) 209 | ._modify( 210 | using: { $0.reverse() }, 211 | undo: { $0.reverse() } 212 | ) 213 | first.value = 2 214 | 215 | XCTAssertEqual(resettable.wrappedValue, [third, second, first]) 216 | 217 | resettable.redo().redo().redo() // no changes 218 | 219 | XCTAssertEqual(resettable.wrappedValue, [third, second, first]) 220 | 221 | resettable.undo() 222 | 223 | XCTAssertEqual(resettable.wrappedValue, [first, second, third]) 224 | 225 | resettable.undo() 226 | 227 | XCTAssertEqual(resettable.wrappedValue, [first, second]) 228 | 229 | resettable.redo() 230 | 231 | XCTAssertEqual(resettable.wrappedValue, [first, second, third]) 232 | 233 | resettable 234 | .collection.remove(at: 2) 235 | .collection.remove(at: 1) 236 | .collection.remove(at: 0) 237 | 238 | resettable.undo().undo().undo() 239 | 240 | XCTAssertEqual(resettable.wrappedValue, [first, second, third]) 241 | 242 | resettable.restore() 243 | 244 | XCTAssertEqual(resettable.wrappedValue, []) 245 | 246 | resettable.reset() 247 | 248 | first.value = 0 249 | second.value = 1 250 | 251 | XCTAssertEqual(resettable.wrappedValue, [first, second]) 252 | } 253 | 254 | func testAmendValueType() { 255 | var value = TestStruct() 256 | let resettable = Resettable(value) 257 | 258 | resettable.inner.value(1) 259 | value.inner.value = 1 260 | 261 | resettable.boolean(true) 262 | value.boolean = true 263 | 264 | resettable.inner.value(2) 265 | value.inner.value = 2 266 | 267 | resettable.int(10) 268 | value.int = 10 269 | 270 | XCTAssertEqual(resettable.wrappedValue, value) 271 | 272 | resettable.undo() 273 | value.int = 0 274 | XCTAssertEqual(resettable.wrappedValue, value) 275 | 276 | resettable.undo() 277 | value.inner.value = 1 278 | XCTAssertEqual(resettable.wrappedValue, value) 279 | 280 | resettable.undo() 281 | value.boolean = false 282 | XCTAssertEqual(resettable.wrappedValue, value) 283 | 284 | resettable.inner.value(100, operation: .amend) 285 | value.inner.value = 100 286 | XCTAssertEqual(resettable.wrappedValue, value) 287 | 288 | resettable.redo() 289 | value.boolean = true 290 | XCTAssertEqual(resettable.wrappedValue, value) 291 | 292 | resettable.redo() 293 | value.inner.value = 2 294 | XCTAssertEqual(resettable.wrappedValue, value) 295 | 296 | resettable.redo() 297 | value.int = 10 298 | XCTAssertEqual(resettable.wrappedValue, value) 299 | 300 | resettable.undo() 301 | value.int = 0 302 | XCTAssertEqual(resettable.wrappedValue, value) 303 | } 304 | 305 | func testAmendReferenceType() { 306 | let value = TestClass() 307 | let resettable = Resettable(value) 308 | 309 | resettable.inner.value(1) 310 | value.inner.value = 1 311 | 312 | resettable.boolean(true) 313 | value.boolean = true 314 | 315 | resettable.inner.value(2) 316 | value.inner.value = 2 317 | 318 | resettable.int(10) 319 | value.int = 10 320 | 321 | XCTAssertEqual(resettable.wrappedValue, value) 322 | 323 | resettable.undo() 324 | value.int = 0 325 | XCTAssertEqual(resettable.wrappedValue, value) 326 | 327 | resettable.undo() 328 | value.inner.value = 1 329 | XCTAssertEqual(resettable.wrappedValue, value) 330 | 331 | resettable.undo() 332 | value.boolean = false 333 | XCTAssertEqual(resettable.wrappedValue, value) 334 | 335 | resettable.inner.value(100, operation: .amend) 336 | value.inner.value = 100 337 | XCTAssertEqual(resettable.wrappedValue, value) 338 | 339 | resettable.redo() 340 | value.boolean = true 341 | XCTAssertEqual(resettable.wrappedValue, value) 342 | 343 | resettable.redo() 344 | value.inner.value = 2 345 | XCTAssertEqual(resettable.wrappedValue, value) 346 | 347 | resettable.redo() 348 | value.int = 10 349 | XCTAssertEqual(resettable.wrappedValue, value) 350 | } 351 | 352 | public func testAmendCollection() { 353 | struct Object: Equatable { 354 | let id: UUID = .init() 355 | var value: Int = 0 356 | } 357 | 358 | let first = Object(value: 0) 359 | var second = Object(value: 1) 360 | let third = Object(value: 2) 361 | let resettable = Resettable([first, second]) 362 | 363 | resettable.collection.swapAt(0, 1) 364 | 365 | XCTAssertEqual(resettable.wrappedValue, [second, first]) 366 | 367 | resettable.collection[safe: 0].value(0) 368 | second.value = 0 369 | 370 | XCTAssertEqual(resettable.wrappedValue, [second, first]) 371 | 372 | resettable.undo() 373 | second.value = 1 374 | XCTAssertEqual(resettable.wrappedValue, [second, first]) 375 | 376 | resettable.undo() 377 | XCTAssertEqual(resettable.wrappedValue, [first, second]) 378 | 379 | resettable.collection.append(third, operation: .insert) 380 | 381 | XCTAssertEqual(resettable.wrappedValue, [first, second, third]) 382 | 383 | resettable.redo() 384 | XCTAssertEqual(resettable.wrappedValue, [second, first, third]) 385 | } 386 | 387 | func testInjectAndDump() { 388 | var value = TestStruct() 389 | let resettable = Resettable(value) 390 | 391 | resettable.int { $0 += 1 } 392 | resettable.int { $0 += 1 } 393 | resettable.int { $0 += 1 } 394 | value.int += 3 395 | 396 | resettable.undo(2) 397 | value.int -= 2 398 | XCTAssertEqual(resettable.wrappedValue, value) 399 | 400 | resettable.int(.inject) { $0 *= 3 } 401 | value.int *= 3 402 | XCTAssertEqual(resettable.wrappedValue, value) 403 | 404 | resettable.restore() 405 | value.int += 1 406 | value.int += 1 407 | XCTAssertEqual(resettable.wrappedValue, value) 408 | 409 | let expectedDump = #""" 410 | """ 411 | ResettableTests.TestStruct( 412 | inner: ResettableTests.TestStruct.Inner(value: 0), 413 | boolean: false, 414 | int: 0, 415 | optional: nil 416 | ) 417 | 418 | ResettableTests.TestStruct( 419 | inner: ResettableTests.TestStruct.Inner(value: 0), 420 | boolean: false, 421 | - int: 0, 422 | + int: 3, 423 | optional: nil 424 | ) 425 | 426 | ResettableTests.TestStruct( 427 | inner: ResettableTests.TestStruct.Inner(value: 0), 428 | boolean: false, 429 | - int: 3, 430 | + int: 4, 431 | optional: nil 432 | ) 433 | 434 | >>> ResettableTests.TestStruct( 435 | inner: ResettableTests.TestStruct.Inner(value: 0), 436 | boolean: false, 437 | - int: 4, 438 | + int: 5, 439 | optional: nil 440 | ) 441 | """ 442 | """# 443 | 444 | let actualDump = resettable.dump() 445 | XCTAssertEqual(expectedDump, actualDump) 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /Tests/FoundationExtensionsTests/SwizzlingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FoundationExtensions 3 | 4 | final class SwizzlingTests: XCTestCase { 5 | func testMain() { 6 | let object = MyObject() 7 | 8 | XCTAssertEqual(object.modify(10), 11) 9 | 10 | MyObject.swizzle() // exchanges implementations 11 | 12 | XCTAssertEqual(object.modify(10), 21) 13 | 14 | MyObject.swizzle() // exchanges implementations back 15 | 16 | XCTAssertEqual(object.modify(10), 11) 17 | } 18 | } 19 | 20 | private class MyObject: NSObject { 21 | @objc dynamic 22 | func modify(_ value: Int) -> Int { 23 | return value + 1 24 | } 25 | } 26 | 27 | extension MyObject { 28 | /// Performs swizzling for the class 29 | /// 30 | /// If you only want to allow to swizzle your class once in app lifetime you can do this 31 | /// ```swift 32 | /// static let swizzle: Void = { 33 | /// objc_exchangeImplementations( 34 | /// #selector(modify), 35 | /// #selector(__swizzledModify) 36 | /// ) 37 | /// }() 38 | /// ``` 39 | static func swizzle() { 40 | objc_exchangeImplementations( 41 | #selector(modify), 42 | #selector(__swizzledModify) 43 | ) 44 | 45 | // add more swizzling here if needed 46 | } 47 | 48 | @objc dynamic 49 | private func __swizzledModify(_ value: Int) -> Int { 50 | // Calls original method 51 | return __swizzledModify(value * 2) 52 | } 53 | } 54 | --------------------------------------------------------------------------------