├── .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 | [](https://github.com/CaptureContext/swift-foundation-extensions/actions/workflows/ci.yml) [](https://swift.org/download/)  [](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