diff --git a/.github/workflows/RadiantKit.yml b/.github/workflows/RadiantKit.yml
new file mode 100644
index 0000000..57e43e1
--- /dev/null
+++ b/.github/workflows/RadiantKit.yml
@@ -0,0 +1,141 @@
+name: RadiantKit
+on:
+ push:
+ branches-ignore:
+ - '*WIP'
+env:
+ PACKAGE_NAME: RadiantKit
+jobs:
+ build-ubuntu:
+ name: Build on Ubuntu
+ env:
+ SWIFT_VER: 6.0
+ if: "!contains(github.event.head_commit.message, 'ci skip')"
+ runs-on: ubuntu-latest
+ container:
+ image: swiftlang/swift:nightly-6.0-jammy
+ steps:
+ - uses: actions/checkout@v4
+ - name: Cache swift package modules
+ id: cache-spm-linux
+ uses: actions/cache@v4
+ env:
+ cache-name: cache-spm
+ with:
+ path: .build
+ key: ${{ runner.os }}-${{ env.cache-name }}-${{ matrix.swift-version }}-${{ hashFiles('Package.resolved') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ env.cache-name }}-${{ matrix.swift-version }}-
+ ${{ runner.os }}-${{ env.cache-name }}-
+ - name: Test
+ run: swift test --enable-code-coverage
+ - uses: sersoft-gmbh/swift-coverage-action@v4
+ id: coverage-files
+ with:
+ fail-on-empty-output: true
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ fail_ci_if_error: true
+ flags: swift-${{ matrix.swift-version }},ubuntu
+ verbose: true
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }}
+ build-macos:
+ name: Build on macOS
+ runs-on: ${{ matrix.os }}
+ if: "!contains(github.event.head_commit.message, 'ci skip')"
+ strategy:
+ matrix:
+ include:
+ - xcode: "/Applications/Xcode_16.1.app"
+ os: macos-14
+ iOSVersion: "18.1"
+ watchOSVersion: "11.0"
+ watchName: "Apple Watch Series 9 (41mm)"
+ iPhoneName: "iPhone 15"
+ steps:
+ - uses: actions/checkout@v4
+ - name: Cache swift package modules
+ id: cache-spm-macos
+ uses: actions/cache@v4
+ env:
+ cache-name: cache-spm
+ with:
+ path: .build
+ key: ${{ matrix.os }}-build-${{ env.cache-name }}-${{ matrix.xcode }}-${{ hashFiles('Package.resolved') }}
+ restore-keys: |
+ ${{ matrix.os }}-build-${{ env.cache-name }}-${{ matrix.xcode }}-
+ - name: Cache mint
+ if: startsWith(matrix.xcode,'/Applications/Xcode_16.1')
+ id: cache-mint
+ uses: actions/cache@v4
+ env:
+ cache-name: cache-mint
+ with:
+ path: .mint
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('Mintfile') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-
+ ${{ runner.os }}-build-
+ ${{ runner.os }}-
+ - name: Set Xcode Name
+ run: echo "XCODE_NAME=$(basename -- ${{ matrix.xcode }} | sed 's/\.[^.]*$//' | cut -d'_' -f2)" >> $GITHUB_ENV
+ - name: Setup Xcode
+ run: sudo xcode-select -s ${{ matrix.xcode }}/Contents/Developer || (sudo ls -1 /Applications | grep "Xcode")
+ - name: Enable Swift Testing
+ run: |
+ mkdir -p ~/Library/org.swift.swiftpm/security/
+ cp macros.json ~/Library/org.swift.swiftpm/security/
+ - name: Install mint
+ if: startsWith(matrix.xcode,'/Applications/Xcode_16.1')
+ run: |
+ brew update
+ brew install mint
+ - name: Build
+ run: swift build
+ - name: Run Swift Package tests
+ run: swift test --enable-code-coverage
+ - uses: sersoft-gmbh/swift-coverage-action@v4
+ id: coverage-files-spm
+ with:
+ fail-on-empty-output: true
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ files: ${{ join(fromJSON(steps.coverage-files-spm.outputs.files), ',') }}
+ token: ${{ secrets.CODECOV_TOKEN }}
+ flags: macOS,${{ env.XCODE_NAME }},${{ matrix.runs-on }}
+ - name: Clean up spm build directory
+ run: rm -rf .build
+ - name: Lint
+ run: ./scripts/lint.sh
+ if: startsWith(matrix.xcode,'/Applications/Xcode_16.1')
+ - name: Run iOS target tests
+ run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }}-Package -destination 'platform=iOS Simulator,name=${{ matrix.iPhoneName }},OS=${{ matrix.iOSVersion }}' -enableCodeCoverage YES build test
+ - uses: sersoft-gmbh/swift-coverage-action@v4
+ id: coverage-files-iOS
+ with:
+ fail-on-empty-output: true
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ fail_ci_if_error: true
+ verbose: true
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ${{ join(fromJSON(steps.coverage-files-iOS.outputs.files), ',') }}
+ flags: iOS,iOS${{ matrix.iOSVersion }},macOS,${{ env.XCODE_NAME }}
+ - name: Run watchOS target tests
+ run: xcodebuild test -scheme ${{ env.PACKAGE_NAME }}-Package -destination 'platform=watchOS Simulator,name=${{ matrix.watchName }},OS=${{ matrix.watchOSVersion }}' -enableCodeCoverage YES build test
+ - uses: sersoft-gmbh/swift-coverage-action@v4
+ id: coverage-files-watchOS
+ with:
+ fail-on-empty-output: true
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ fail_ci_if_error: true
+ verbose: true
+ token: ${{ secrets.CODECOV_TOKEN }}
+ files: ${{ join(fromJSON(steps.coverage-files-watchOS.outputs.files), ',') }}
+ flags: watchOS,watchOS${{ matrix.watchOSVersion }},macOS,${{ env.XCODE_NAME }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..92b69d6
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,78 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches-ignore:
+ - '*WIP'
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ "main" ]
+ schedule:
+ - cron: '20 11 * * 3'
+
+jobs:
+ analyze:
+ if: false
+ name: Analyze
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
+ # - https://gh.io/supported-runners-and-hardware-resources
+ # - https://gh.io/using-larger-runners
+ # Consider using larger runners for possible analysis time improvements.
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-13') || 'ubuntu-latest' }}
+ timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'swift' ]
+ # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
+ # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
+ # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+ # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Xcode
+ run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - run: |
+ echo "Run, Build Application using script"
+ swift build
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..424ea0c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,146 @@
+# Created by https://www.toptal.com/developers/gitignore/api/swift,swiftpm,swiftpackagemanager,xcode,macos
+# Edit at https://www.toptal.com/developers/gitignore?templates=swift,swiftpm,swiftpackagemanager,xcode,macos
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Swift ###
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+*.xcodeproj
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+.swiftpm
+
+.build/
+
+# CocoaPods
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+# Pods/
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+# Accio dependency management
+Dependencies/
+.accio/
+
+# fastlane
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+
+### SwiftPackageManager ###
+# Packages
+xcuserdata
+*.xcodeproj
+
+
+### SwiftPM ###
+
+
+### Xcode ###
+
+## Xcode 8 and earlier
+
+### Xcode Patch ###
+*.xcodeproj/*
+!*.xcodeproj/project.pbxproj
+!*.xcodeproj/xcshareddata/
+!*.xcodeproj/project.xcworkspace/
+!*.xcworkspace/contents.xcworkspacedata
+/*.gcno
+**/xcshareddata/WorkspaceSettings.xcsettings
+.mint
+# End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,swiftpackagemanager,xcode,macos
+
+test_output.log
+.docc-build
\ No newline at end of file
diff --git a/.periphery.yml b/.periphery.yml
new file mode 100644
index 0000000..85b884a
--- /dev/null
+++ b/.periphery.yml
@@ -0,0 +1 @@
+retain_public: true
diff --git a/.spi.yml b/.spi.yml
new file mode 100644
index 0000000..d6334bf
--- /dev/null
+++ b/.spi.yml
@@ -0,0 +1,5 @@
+version: 1
+builder:
+ configs:
+ - documentation_targets: [RadiantKit]
+ swift_version: 6.0
diff --git a/.swift-format b/.swift-format
new file mode 100644
index 0000000..4f562bf
--- /dev/null
+++ b/.swift-format
@@ -0,0 +1,70 @@
+{
+ "fileScopedDeclarationPrivacy" : {
+ "accessLevel" : "fileprivate"
+ },
+ "indentation" : {
+ "spaces" : 2
+ },
+ "indentConditionalCompilationBlocks" : true,
+ "indentSwitchCaseLabels" : true,
+ "lineBreakAroundMultilineExpressionChainComponents" : true,
+ "lineBreakBeforeControlFlowKeywords" : true,
+ "lineBreakBeforeEachArgument" : true,
+ "lineBreakBeforeEachGenericRequirement" : true,
+ "lineLength" : 100,
+ "maximumBlankLines" : 1,
+ "multiElementCollectionTrailingCommas" : true,
+ "noAssignmentInExpressions" : {
+ "allowedFunctions" : [
+ "XCTAssertNoThrow"
+ ]
+ },
+ "prioritizeKeepingFunctionOutputTogether" : false,
+ "respectsExistingLineBreaks" : false,
+ "rules" : {
+ "AllPublicDeclarationsHaveDocumentation" : true,
+ "AlwaysUseLiteralForEmptyCollectionInit" : false,
+ "AlwaysUseLowerCamelCase" : true,
+ "AmbiguousTrailingClosureOverload" : true,
+ "BeginDocumentationCommentWithOneLineSummary" : false,
+ "DoNotUseSemicolons" : true,
+ "DontRepeatTypeInStaticProperties" : true,
+ "FileScopedDeclarationPrivacy" : true,
+ "FullyIndirectEnum" : true,
+ "GroupNumericLiterals" : true,
+ "IdentifiersMustBeASCII" : true,
+ "NeverForceUnwrap" : true,
+ "NeverUseForceTry" : true,
+ "NeverUseImplicitlyUnwrappedOptionals" : true,
+ "NoAccessLevelOnExtensionDeclaration" : true,
+ "NoAssignmentInExpressions" : true,
+ "NoBlockComments" : true,
+ "NoCasesWithOnlyFallthrough" : true,
+ "NoEmptyTrailingClosureParentheses" : true,
+ "NoLabelsInCasePatterns" : true,
+ "NoLeadingUnderscores" : false,
+ "NoParensAroundConditions" : true,
+ "NoPlaygroundLiterals" : true,
+ "NoVoidReturnOnFunctionSignature" : true,
+ "OmitExplicitReturns" : false,
+ "OneCasePerLine" : true,
+ "OneVariableDeclarationPerLine" : true,
+ "OnlyOneTrailingClosureArgument" : true,
+ "OrderedImports" : true,
+ "ReplaceForEachWithForLoop" : true,
+ "ReturnVoidInsteadOfEmptyTuple" : true,
+ "TypeNamesShouldBeCapitalized" : true,
+ "UseEarlyExits" : false,
+ "UseExplicitNilCheckInConditions" : true,
+ "UseLetInEveryBoundCaseVariable" : true,
+ "UseShorthandTypeNames" : true,
+ "UseSingleLinePropertyGetter" : true,
+ "UseSynthesizedInitializer" : true,
+ "UseTripleSlashForDocumentationComments" : true,
+ "UseWhereClausesInForLoops" : true,
+ "ValidateDocumentationComments" : true
+ },
+ "spacesAroundRangeFormationOperators" : false,
+ "tabWidth" : 2,
+ "version" : 1
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..575c376
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 BrightDigit
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Mintfile b/Mintfile
new file mode 100644
index 0000000..7060932
--- /dev/null
+++ b/Mintfile
@@ -0,0 +1,2 @@
+apple/swift-format@4b62459
+peripheryapp/periphery@2.20.0
\ No newline at end of file
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..d4ee4e8
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,33 @@
+{
+ "originHash" : "0cd2dcc1e220681712709df5bd794ab7f349d718bc63840353fae6e361bc50c4",
+ "pins" : [
+ {
+ "identity" : "swift-log",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-log.git",
+ "state" : {
+ "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537",
+ "version" : "1.6.1"
+ }
+ },
+ {
+ "identity" : "swift-syntax",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swiftlang/swift-syntax.git",
+ "state" : {
+ "revision" : "515f79b522918f83483068d99c68daeb5116342d",
+ "version" : "600.0.0-prerelease-2024-09-04"
+ }
+ },
+ {
+ "identity" : "swift-testing",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swiftlang/swift-testing.git",
+ "state" : {
+ "revision" : "c55848b2aa4b29a4df542b235dfdd792a6fbe341",
+ "version" : "0.12.0"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..01e26a4
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,86 @@
+// swift-tools-version: 6.0
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let swiftSettings: [SwiftSetting] = [
+ SwiftSetting.enableExperimentalFeature("AccessLevelOnImport"),
+ SwiftSetting.enableExperimentalFeature("BitwiseCopyable"),
+ SwiftSetting.enableExperimentalFeature("GlobalActorIsolatedTypesUsability"),
+ SwiftSetting.enableExperimentalFeature("IsolatedAny"),
+ SwiftSetting.enableExperimentalFeature("MoveOnlyPartialConsumption"),
+ SwiftSetting.enableExperimentalFeature("NestedProtocols"),
+ SwiftSetting.enableExperimentalFeature("NoncopyableGenerics"),
+ SwiftSetting.enableExperimentalFeature("RegionBasedIsolation"),
+ SwiftSetting.enableExperimentalFeature("TransferringArgsAndResults"),
+ SwiftSetting.enableExperimentalFeature("VariadicGenerics"),
+
+ SwiftSetting.enableUpcomingFeature("FullTypedThrows"),
+ SwiftSetting.enableUpcomingFeature("InternalImportsByDefault"),
+
+ // SwiftSetting.unsafeFlags([
+ // "-Xfrontend",
+ // "-warn-long-function-bodies=100"
+ // ]),
+ // SwiftSetting.unsafeFlags([
+ // "-Xfrontend",
+ // "-warn-long-expression-type-checking=100"
+ // ])
+]
+
+let package = Package(
+ name: "RadiantKit",
+ platforms: [.iOS(.v17), .macCatalyst(.v17), .macOS(.v14), .tvOS(.v17), .visionOS(.v1), .watchOS(.v10)],
+ products: [
+ .library(
+ name: "RadiantKit",
+ targets: ["RadiantKit"]
+ ),
+ .library(
+ name: "RadiantDocs",
+ targets: ["RadiantDocs"]
+ ),
+ .library(
+ name: "RadiantPaging",
+ targets: ["RadiantPaging"]
+ ),
+ .library(
+ name: "RadiantProgress",
+ targets: ["RadiantProgress"]
+ )
+ ],
+ dependencies: [
+ .package(url: "https://github.com/swiftlang/swift-testing.git", from: "0.12.0"),
+ .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
+ ],
+ targets: [
+ .target(
+ name: "RadiantKit",
+ swiftSettings: swiftSettings
+ ),
+ .target(
+ name: "RadiantDocs",
+ dependencies: ["RadiantKit"],
+ swiftSettings: swiftSettings
+ ),
+ .target(
+ name: "RadiantPaging",
+ dependencies: ["RadiantKit"],
+ swiftSettings: swiftSettings
+ ),
+ .target(
+ name: "RadiantProgress",
+ dependencies: [
+ .product(name: "Logging", package: "swift-log", condition: .when(platforms: [.linux]))
+ ],
+ swiftSettings: swiftSettings
+ ),
+ .testTarget(
+ name: "RadiantKitTests",
+ dependencies: [
+ "RadiantKit",
+ .product(name: "Testing", package: "swift-testing")
+ ]
+ )
+ ]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e754176
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+# RadiantKit
+
+[![](https://img.shields.io/badge/docc-read_documentation-blue)](https://swiftpackageindex.com/brightdigit/RadiantKit/documentation)
+[![SwiftPM](https://img.shields.io/badge/SPM-Linux%20%7C%20iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS-success?logo=swift)](https://swift.org)
+[![Twitter](https://img.shields.io/badge/twitter-@brightdigit-blue.svg?style=flat)](http://twitter.com/brightdigit)
+![GitHub](https://img.shields.io/github/license/brightdigit/RadiantKit)
+![GitHub issues](https://img.shields.io/github/issues/brightdigit/RadiantKit)
+![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/brightdigit/RadiantKit/RadiantKit.yml?label=actions&logo=github&?branch=main)
+
+[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FRadiantKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/brightdigit/RadiantKit)
+[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fbrightdigit%2FRadiantKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/brightdigit/RadiantKit)
+
+[![Codecov](https://img.shields.io/codecov/c/github/brightdigit/RadiantKit)](https://codecov.io/gh/brightdigit/RadiantKit)
+[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/brightdigit/RadiantKit)](https://www.codefactor.io/repository/github/brightdigit/RadiantKit)
+[![codebeat badge](https://codebeat.co/badges/ba6d29a8-1d14-4c0c-9993-5cad98664300)](https://codebeat.co/projects/github-com-brightdigit-RadiantKit-main)
+[![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/brightdigit/RadiantKit)](https://codeclimate.com/github/brightdigit/RadiantKit)
+[![Code Climate technical debt](https://img.shields.io/codeclimate/tech-debt/brightdigit/RadiantKit?label=debt)](https://codeclimate.com/github/brightdigit/RadiantKit)
+[![Code Climate issues](https://img.shields.io/codeclimate/issues/brightdigit/RadiantKit)](https://codeclimate.com/github/brightdigit/RadiantKit)
diff --git a/Scripts/gh-md-toc b/Scripts/gh-md-toc
new file mode 100755
index 0000000..03b5ddd
--- /dev/null
+++ b/Scripts/gh-md-toc
@@ -0,0 +1,421 @@
+#!/usr/bin/env bash
+
+#
+# Steps:
+#
+# 1. Download corresponding html file for some README.md:
+# curl -s $1
+#
+# 2. Discard rows where no substring 'user-content-' (github's markup):
+# awk '/user-content-/ { ...
+#
+# 3.1 Get last number in each row like ' ... sitemap.js.*<\/h/)+2, RLENGTH-5)
+#
+# 5. Find anchor and insert it inside "(...)":
+# substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8)
+#
+
+gh_toc_version="0.10.0"
+
+gh_user_agent="gh-md-toc v$gh_toc_version"
+
+#
+# Download rendered into html README.md by its url.
+#
+#
+gh_toc_load() {
+ local gh_url=$1
+
+ if type curl &>/dev/null; then
+ curl --user-agent "$gh_user_agent" -s "$gh_url"
+ elif type wget &>/dev/null; then
+ wget --user-agent="$gh_user_agent" -qO- "$gh_url"
+ else
+ echo "Please, install 'curl' or 'wget' and try again."
+ exit 1
+ fi
+}
+
+#
+# Converts local md file into html by GitHub
+#
+# -> curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown
+#
Hello world github/linguist#1 cool, and #1!
'"
+gh_toc_md2html() {
+ local gh_file_md=$1
+ local skip_header=$2
+
+ URL=https://api.github.com/markdown/raw
+
+ if [ -n "$GH_TOC_TOKEN" ]; then
+ TOKEN=$GH_TOC_TOKEN
+ else
+ TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
+ if [ -f "$TOKEN_FILE" ]; then
+ TOKEN="$(cat "$TOKEN_FILE")"
+ fi
+ fi
+ if [ -n "${TOKEN}" ]; then
+ AUTHORIZATION="Authorization: token ${TOKEN}"
+ fi
+
+ local gh_tmp_file_md=$gh_file_md
+ if [ "$skip_header" = "yes" ]; then
+ if grep -Fxq "" "$gh_src"; then
+ # cut everything before the toc
+ gh_tmp_file_md=$gh_file_md~~
+ sed '1,//d' "$gh_file_md" > "$gh_tmp_file_md"
+ fi
+ fi
+
+ # echo $URL 1>&2
+ OUTPUT=$(curl -s \
+ --user-agent "$gh_user_agent" \
+ --data-binary @"$gh_tmp_file_md" \
+ -H "Content-Type:text/plain" \
+ -H "$AUTHORIZATION" \
+ "$URL")
+
+ rm -f "${gh_file_md}~~"
+
+ if [ "$?" != "0" ]; then
+ echo "XXNetworkErrorXX"
+ fi
+ if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then
+ echo "XXRateLimitXX"
+ else
+ echo "${OUTPUT}"
+ fi
+}
+
+
+#
+# Is passed string url
+#
+gh_is_url() {
+ case $1 in
+ https* | http*)
+ echo "yes";;
+ *)
+ echo "no";;
+ esac
+}
+
+#
+# TOC generator
+#
+gh_toc(){
+ local gh_src=$1
+ local gh_src_copy=$1
+ local gh_ttl_docs=$2
+ local need_replace=$3
+ local no_backup=$4
+ local no_footer=$5
+ local indent=$6
+ local skip_header=$7
+
+ if [ "$gh_src" = "" ]; then
+ echo "Please, enter URL or local path for a README.md"
+ exit 1
+ fi
+
+
+ # Show "TOC" string only if working with one document
+ if [ "$gh_ttl_docs" = "1" ]; then
+
+ echo "Table of Contents"
+ echo "================="
+ echo ""
+ gh_src_copy=""
+
+ fi
+
+ if [ "$(gh_is_url "$gh_src")" == "yes" ]; then
+ gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy" "$indent"
+ if [ "${PIPESTATUS[0]}" != "0" ]; then
+ echo "Could not load remote document."
+ echo "Please check your url or network connectivity"
+ exit 1
+ fi
+ if [ "$need_replace" = "yes" ]; then
+ echo
+ echo "!! '$gh_src' is not a local file"
+ echo "!! Can't insert the TOC into it."
+ echo
+ fi
+ else
+ local rawhtml
+ rawhtml=$(gh_toc_md2html "$gh_src" "$skip_header")
+ if [ "$rawhtml" == "XXNetworkErrorXX" ]; then
+ echo "Parsing local markdown file requires access to github API"
+ echo "Please make sure curl is installed and check your network connectivity"
+ exit 1
+ fi
+ if [ "$rawhtml" == "XXRateLimitXX" ]; then
+ echo "Parsing local markdown file requires access to github API"
+ echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting"
+ TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
+ echo "or place GitHub auth token here: ${TOKEN_FILE}"
+ exit 1
+ fi
+ local toc
+ toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy" "$indent"`
+ echo "$toc"
+ if [ "$need_replace" = "yes" ]; then
+ if grep -Fxq "" "$gh_src" && grep -Fxq "" "$gh_src"; then
+ echo "Found markers"
+ else
+ echo "You don't have or in your file...exiting"
+ exit 1
+ fi
+ local ts="<\!--ts-->"
+ local te="<\!--te-->"
+ local dt
+ dt=$(date +'%F_%H%M%S')
+ local ext=".orig.${dt}"
+ local toc_path="${gh_src}.toc.${dt}"
+ local toc_createdby=""
+ local toc_footer
+ toc_footer=""
+ # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html
+ # clear old TOC
+ sed -i"${ext}" "/${ts}/,/${te}/{//!d;}" "$gh_src"
+ # create toc file
+ echo "${toc}" > "${toc_path}"
+ if [ "${no_footer}" != "yes" ]; then
+ echo -e "\n${toc_createdby}\n${toc_footer}\n" >> "$toc_path"
+ fi
+
+ # insert toc file
+ if ! sed --version > /dev/null 2>&1; then
+ sed -i "" "/${ts}/r ${toc_path}" "$gh_src"
+ else
+ sed -i "/${ts}/r ${toc_path}" "$gh_src"
+ fi
+ echo
+ if [ "${no_backup}" = "yes" ]; then
+ rm "$toc_path" "$gh_src$ext"
+ fi
+ echo "!! TOC was added into: '$gh_src'"
+ if [ -z "${no_backup}" ]; then
+ echo "!! Origin version of the file: '${gh_src}${ext}'"
+ echo "!! TOC added into a separate file: '${toc_path}'"
+ fi
+ echo
+ fi
+ fi
+}
+
+#
+# Grabber of the TOC from rendered html
+#
+# $1 - a source url of document.
+# It's need if TOC is generated for multiple documents.
+# $2 - number of spaces used to indent.
+#
+gh_toc_grab() {
+
+ href_regex="/href=\"[^\"]+?\"/"
+ common_awk_script='
+ modified_href = ""
+ split(href, chars, "")
+ for (i=1;i <= length(href); i++) {
+ c = chars[i]
+ res = ""
+ if (c == "+") {
+ res = " "
+ } else {
+ if (c == "%") {
+ res = "\\x"
+ } else {
+ res = c ""
+ }
+ }
+ modified_href = modified_href res
+ }
+ print sprintf("%*s", (level-1)*'"$2"', "") "* [" text "](" gh_url modified_href ")"
+ '
+ if [ "`uname -s`" == "OS/390" ]; then
+ grepcmd="pcregrep -o"
+ echoargs=""
+ awkscript='{
+ level = substr($0, 3, 1)
+ text = substr($0, match($0, /<\/span><\/a>[^<]*<\/h/)+11, RLENGTH-14)
+ href = substr($0, match($0, '$href_regex')+6, RLENGTH-7)
+ '"$common_awk_script"'
+ }'
+ else
+ grepcmd="grep -Eo"
+ echoargs="-e"
+ awkscript='{
+ level = substr($0, 3, 1)
+ text = substr($0, match($0, /">.*<\/h/)+2, RLENGTH-5)
+ href = substr($0, match($0, '$href_regex')+6, RLENGTH-7)
+ '"$common_awk_script"'
+ }'
+ fi
+
+ # if closed is on the new line, then move it on the prev line
+ # for example:
+ # was: The command foo1
+ #
+ # became: The command foo1
+ sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' |
+
+ # Sometimes a line can start with . Fix that.
+ sed -e ':a' -e 'N' -e '$!ba' -e 's/\n//g' | sed 's/<\/code>//g' |
+
+ # remove g-emoji
+ sed 's/]*[^<]*<\/g-emoji> //g' |
+
+ # now all rows are like:
+ # title
..
+ # format result line
+ # * $0 - whole string
+ # * last element of each row: "/dev/null; then
+ $tool --version | head -n 1
+ else
+ echo "not installed"
+ fi
+ done
+}
+
+show_help() {
+ local app_name
+ app_name=$(basename "$0")
+ echo "GitHub TOC generator ($app_name): $gh_toc_version"
+ echo ""
+ echo "Usage:"
+ echo " $app_name [options] src [src] Create TOC for a README file (url or local path)"
+ echo " $app_name - Create TOC for markdown from STDIN"
+ echo " $app_name --help Show help"
+ echo " $app_name --version Show version"
+ echo ""
+ echo "Options:"
+ echo " --indent Set indent size. Default: 3."
+ echo " --insert Insert new TOC into original file. For local files only. Default: false."
+ echo " See https://github.com/ekalinin/github-markdown-toc/issues/41 for details."
+ echo " --no-backup Remove backup file. Set --insert as well. Default: false."
+ echo " --hide-footer Do not write date & author of the last TOC update. Set --insert as well. Default: false."
+ echo " --skip-header Hide entry of the topmost headlines. Default: false."
+ echo " See https://github.com/ekalinin/github-markdown-toc/issues/125 for details."
+ echo ""
+}
+
+#
+# Options handlers
+#
+gh_toc_app() {
+ local need_replace="no"
+ local indent=3
+
+ if [ "$1" = '--help' ] || [ $# -eq 0 ] ; then
+ show_help
+ return
+ fi
+
+ if [ "$1" = '--version' ]; then
+ show_version
+ return
+ fi
+
+ if [ "$1" = '--indent' ]; then
+ indent="$2"
+ shift 2
+ fi
+
+ if [ "$1" = "-" ]; then
+ if [ -z "$TMPDIR" ]; then
+ TMPDIR="/tmp"
+ elif [ -n "$TMPDIR" ] && [ ! -d "$TMPDIR" ]; then
+ mkdir -p "$TMPDIR"
+ fi
+ local gh_tmp_md
+ if [ "`uname -s`" == "OS/390" ]; then
+ local timestamp
+ timestamp=$(date +%m%d%Y%H%M%S)
+ gh_tmp_md="$TMPDIR/tmp.$timestamp"
+ else
+ gh_tmp_md=$(mktemp "$TMPDIR/tmp.XXXXXX")
+ fi
+ while read -r input; do
+ echo "$input" >> "$gh_tmp_md"
+ done
+ gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" "$indent"
+ return
+ fi
+
+ if [ "$1" = '--insert' ]; then
+ need_replace="yes"
+ shift
+ fi
+
+ if [ "$1" = '--no-backup' ]; then
+ need_replace="yes"
+ no_backup="yes"
+ shift
+ fi
+
+ if [ "$1" = '--hide-footer' ]; then
+ need_replace="yes"
+ no_footer="yes"
+ shift
+ fi
+
+ if [ "$1" = '--skip-header' ]; then
+ skip_header="yes"
+ shift
+ fi
+
+
+ for md in "$@"
+ do
+ echo ""
+ gh_toc "$md" "$#" "$need_replace" "$no_backup" "$no_footer" "$indent" "$skip_header"
+ done
+
+ echo ""
+ echo ""
+}
+
+#
+# Entry point
+#
+gh_toc_app "$@"
\ No newline at end of file
diff --git a/Scripts/header.sh b/Scripts/header.sh
new file mode 100755
index 0000000..4ed7446
--- /dev/null
+++ b/Scripts/header.sh
@@ -0,0 +1,98 @@
+#!/bin/bash
+
+# Function to print usage
+usage() {
+ echo "Usage: $0 -d directory -c creator -o company -p package [-y year]"
+ echo " -d directory Directory to read from (including subdirectories)"
+ echo " -c creator Name of the creator"
+ echo " -o company Name of the company with the copyright"
+ echo " -p package Package or library name"
+ echo " -y year Copyright year (optional, defaults to current year)"
+ exit 1
+}
+
+# Get the current year if not provided
+current_year=$(date +"%Y")
+
+# Default values
+year="$current_year"
+
+# Parse arguments
+while getopts ":d:c:o:p:y:" opt; do
+ case $opt in
+ d) directory="$OPTARG" ;;
+ c) creator="$OPTARG" ;;
+ o) company="$OPTARG" ;;
+ p) package="$OPTARG" ;;
+ y) year="$OPTARG" ;;
+ *) usage ;;
+ esac
+done
+
+# Check for mandatory arguments
+if [ -z "$directory" ] || [ -z "$creator" ] || [ -z "$company" ] || [ -z "$package" ]; then
+ usage
+fi
+
+# Define the header template
+header_template="//
+// %s
+// %s
+//
+// Created by %s.
+// Copyright © %s %s.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//"
+
+# Loop through each Swift file in the specified directory and subdirectories
+find "$directory" -type f -name "*.swift" | while read -r file; do
+ # Check if the first line is the swift-format-ignore indicator
+ first_line=$(head -n 1 "$file")
+ if [[ "$first_line" == "// swift-format-ignore-file" ]]; then
+ echo "Skipping $file due to swift-format-ignore directive."
+ continue
+ fi
+
+ # Create the header with the current filename
+ filename=$(basename "$file")
+ header=$(printf "$header_template" "$filename" "$package" "$creator" "$year" "$company")
+
+ # Remove all consecutive lines at the beginning which start with "// ", contain only whitespace, or only "//"
+ awk '
+ BEGIN { skip = 1 }
+ {
+ if (skip && ($0 ~ /^\/\/ / || $0 ~ /^\/\/$/ || $0 ~ /^$/)) {
+ next
+ }
+ skip = 0
+ print
+ }' "$file" > temp_file
+
+ # Add the header to the cleaned file
+ (echo "$header"; echo; cat temp_file) > "$file"
+
+ # Remove the temporary file
+ rm temp_file
+done
+
+echo "Headers added or files skipped appropriately across all Swift files in the directory and subdirectories."
\ No newline at end of file
diff --git a/Scripts/lint.sh b/Scripts/lint.sh
new file mode 100755
index 0000000..7f34454
--- /dev/null
+++ b/Scripts/lint.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+if [ "$ACTION" == "install" ]; then
+ if [ -n "$SRCROOT" ]; then
+ exit
+ fi
+fi
+
+export MINT_PATH="$PWD/.mint"
+MINT_ARGS="-n -m Mintfile --silent"
+MINT_RUN="/opt/homebrew/bin/mint run $MINT_ARGS"
+
+if [ -z "$SRCROOT" ] || [ -n "$CHILD_PACKAGE" ]; then
+ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+ PACKAGE_DIR="${SCRIPT_DIR}/.."
+ PERIPHERY_OPTIONS=""
+else
+ PACKAGE_DIR="${SRCROOT}"
+ PERIPHERY_OPTIONS=""
+fi
+
+
+if [ "$LINT_MODE" == "NONE" ]; then
+ exit
+elif [ "$LINT_MODE" == "STRICT" ]; then
+ SWIFTFORMAT_OPTIONS="--strict"
+else
+ SWIFTFORMAT_OPTIONS=""
+fi
+
+/opt/homebrew/bin/mint bootstrap
+
+echo "LINT Mode is $LINT_MODE"
+
+if [ "$LINT_MODE" == "INSTALL" ]; then
+ exit
+fi
+
+if [ -z "$CI" ]; then
+ $MINT_RUN swift-format format --recursive --parallel --in-place $PACKAGE_DIR/Sources
+else
+ set -e
+fi
+
+$PACKAGE_DIR/scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "RadiantKit"
+$MINT_RUN swift-format lint --recursive --parallel $SWIFTFORMAT_OPTIONS $PACKAGE_DIR/Sources
+
+pushd $PACKAGE_DIR
+$MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check
+popd
\ No newline at end of file
diff --git a/Sources/RadiantDocs/Actions/OpenFileURLAction.swift b/Sources/RadiantDocs/Actions/OpenFileURLAction.swift
new file mode 100644
index 0000000..0f90f25
--- /dev/null
+++ b/Sources/RadiantDocs/Actions/OpenFileURLAction.swift
@@ -0,0 +1,65 @@
+//
+// OpenFileURLAction.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ #if os(macOS) || os(iOS) || os(visionOS)
+ public import Foundation
+
+ public import SwiftUI
+
+ fileprivate struct OpenFileURLKey: EnvironmentKey, Sendable {
+ typealias Value = OpenFileURLAction
+
+ static let defaultValue: OpenFileURLAction = .default
+ }
+
+ public typealias OpenWindowURLAction = OpenWindowWithValueAction
+
+ public typealias OpenFileURLAction = OpenWindowURLAction
+
+ extension EnvironmentValues {
+ public var openFileURL: OpenFileURLAction {
+ get { self[OpenFileURLKey.self] }
+ set { self[OpenFileURLKey.self] = newValue }
+ }
+ }
+
+ extension Scene {
+ public func openFileURL(
+ _ closure: @escaping @Sendable @MainActor (URL, OpenWindowAction) -> Void
+ ) -> some Scene { self.environment(\.openFileURL, .init(closure: closure)) }
+ }
+
+ @available(*, deprecated, message: "Use on Scene only.") extension View {
+ public func openFileURL(
+ _ closure: @Sendable @escaping @MainActor (URL, OpenWindowAction) -> Void
+ ) -> some View { self.environment(\.openFileURL, .init(closure: closure)) }
+ }
+ #endif
+#endif
diff --git a/Sources/RadiantDocs/Actions/OpenWindowWithAction.swift b/Sources/RadiantDocs/Actions/OpenWindowWithAction.swift
new file mode 100644
index 0000000..331e3c5
--- /dev/null
+++ b/Sources/RadiantDocs/Actions/OpenWindowWithAction.swift
@@ -0,0 +1,48 @@
+//
+// OpenWindowWithAction.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ #if os(macOS) || os(iOS) || os(visionOS)
+ import Foundation
+
+ public import SwiftUI
+
+ public typealias OpenWindowWithAction = OpenWindowWithValueAction
+
+ @MainActor extension OpenWindowWithAction {
+ public init(closure: @escaping @Sendable @MainActor (OpenWindowAction) -> Void) {
+ self.init { _, action in closure(action) }
+ }
+
+ @MainActor public func callAsFunction(with openWidow: OpenWindowAction) {
+ closure((), openWidow)
+ }
+ }
+ #endif
+#endif
diff --git a/Sources/RadiantDocs/Actions/OpenWindowWithValueAction.swift b/Sources/RadiantDocs/Actions/OpenWindowWithValueAction.swift
new file mode 100644
index 0000000..32f3f6d
--- /dev/null
+++ b/Sources/RadiantDocs/Actions/OpenWindowWithValueAction.swift
@@ -0,0 +1,55 @@
+//
+// OpenWindowWithValueAction.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ #if os(macOS) || os(iOS) || os(visionOS)
+ import Foundation
+
+ public import SwiftUI
+
+ public struct OpenWindowWithValueAction: Sendable {
+ public static var `default`: Self {
+ .init { value, action in defaultClosure(value, with: action) }
+ }
+
+ let closure: @Sendable @MainActor (ValueType, OpenWindowAction) -> Void
+ public init(closure: @escaping @MainActor @Sendable (ValueType, OpenWindowAction) -> Void) {
+ self.closure = closure
+ }
+
+ private static func defaultClosure(_: ValueType, with _: OpenWindowAction) {
+ assertionFailure()
+ }
+
+ @MainActor public func callAsFunction(_ value: ValueType, with openWidow: OpenWindowAction) {
+ closure(value, openWidow)
+ }
+ }
+ #endif
+#endif
diff --git a/Sources/RadiantDocs/AllowedOpenFileTypesKey.swift b/Sources/RadiantDocs/AllowedOpenFileTypesKey.swift
new file mode 100644
index 0000000..4d6477f
--- /dev/null
+++ b/Sources/RadiantDocs/AllowedOpenFileTypesKey.swift
@@ -0,0 +1,61 @@
+//
+// AllowedOpenFileTypesKey.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ import Foundation
+
+ public import SwiftUI
+
+ import UniformTypeIdentifiers
+
+ fileprivate struct AllowedOpenFileTypesKey: EnvironmentKey {
+ typealias Value = [FileType]
+ static let defaultValue = [FileType]()
+ }
+
+ extension EnvironmentValues {
+ public var allowedOpenFileTypes: [FileType] {
+ get { self[AllowedOpenFileTypesKey.self] }
+ set { self[AllowedOpenFileTypesKey.self] = newValue }
+ }
+ }
+
+ @available(*, deprecated, message: "Use on Scene only.") extension View {
+ public func allowedOpenFileTypes(_ fileTypes: [FileType]) -> some View {
+ self.environment(\.allowedOpenFileTypes, fileTypes)
+ }
+ }
+
+ extension Scene {
+ public func allowedOpenFileTypes(_ fileTypes: [FileType]) -> some Scene {
+ self.environment(\.allowedOpenFileTypes, fileTypes)
+ }
+ }
+
+#endif
diff --git a/Sources/RadiantDocs/AppKit/NewFilePanel.swift b/Sources/RadiantDocs/AppKit/NewFilePanel.swift
new file mode 100644
index 0000000..bbb0436
--- /dev/null
+++ b/Sources/RadiantDocs/AppKit/NewFilePanel.swift
@@ -0,0 +1,67 @@
+//
+// NewFilePanel.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(AppKit) && canImport(SwiftUI)
+ import AppKit
+
+ import Foundation
+
+ public import SwiftUI
+
+ import UniformTypeIdentifiers
+
+ public struct NewFilePanel: Sendable {
+ public init() {}
+
+ public init(_: FileType.Type) {}
+
+ @MainActor public func callAsFunction(with openWindow: OpenWindowAction) {
+ let openPanel = NSSavePanel()
+ openPanel.allowedContentTypes = [UTType(fileType: FileType.fileType)]
+ openPanel.isExtensionHidden = true
+ openPanel.begin { response in
+ guard let fileURL = openPanel.url, response == .OK else { return }
+ let value: FileType.WindowValueType
+ do { value = try FileType.createAt(fileURL) }
+ catch {
+ openPanel.presentError(error)
+ return
+ }
+ openWindow(value: value)
+ }
+ }
+ }
+
+ extension OpenWindowAction {
+ @MainActor public func callAsFunction(
+ newFileOf valueType: (some InitializableFileTypeSpecification).Type
+ ) { NewFilePanel(valueType)(with: self) }
+ }
+
+#endif
diff --git a/Sources/RadiantDocs/AppKit/OpenAnyFilePanel.swift b/Sources/RadiantDocs/AppKit/OpenAnyFilePanel.swift
new file mode 100644
index 0000000..3c4ebcd
--- /dev/null
+++ b/Sources/RadiantDocs/AppKit/OpenAnyFilePanel.swift
@@ -0,0 +1,68 @@
+//
+// OpenAnyFilePanel.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(AppKit) && canImport(SwiftUI)
+ import AppKit
+
+ import Foundation
+
+ public import SwiftUI
+
+ import UniformTypeIdentifiers
+
+ public struct OpenAnyFilePanel {
+ let fileTypes: [FileType]
+
+ internal init(fileTypes: [FileType]) {
+ assert(!fileTypes.isEmpty)
+ self.fileTypes = fileTypes
+ }
+
+ @MainActor public func callAsFunction(
+ with openFileURL: OpenFileURLAction,
+ using openWindow: OpenWindowAction
+ ) {
+ let openPanel = NSOpenPanel()
+ openPanel.allowedContentTypes = fileTypes.map(UTType.init(fileType:))
+ openPanel.isExtensionHidden = true
+ openPanel.begin { response in
+ guard let fileURL = openPanel.url, response == .OK else { return }
+ openFileURL(fileURL, with: openWindow)
+ }
+ }
+ }
+
+ extension OpenFileURLAction {
+ @MainActor public func callAsFunction(
+ ofFileTypes fileTypes: [FileType],
+ using openWindow: OpenWindowAction
+ ) { OpenAnyFilePanel(fileTypes: fileTypes).callAsFunction(with: self, using: openWindow) }
+ }
+
+#endif
diff --git a/Sources/RadiantDocs/AppKit/OpenFilePanel.swift b/Sources/RadiantDocs/AppKit/OpenFilePanel.swift
new file mode 100644
index 0000000..4a2b0db
--- /dev/null
+++ b/Sources/RadiantDocs/AppKit/OpenFilePanel.swift
@@ -0,0 +1,65 @@
+//
+// OpenFilePanel.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(AppKit) && canImport(SwiftUI)
+ import AppKit
+
+ import Foundation
+
+ public import SwiftUI
+
+ import UniformTypeIdentifiers
+
+ public struct OpenFilePanel: Sendable {
+ public init() {}
+
+ public init(_: FileType.Type) {}
+
+ @MainActor public func callAsFunction(with openWindow: OpenWindowAction) {
+ let openPanel = NSOpenPanel()
+ openPanel.allowedContentTypes = [UTType(fileType: FileType.fileType)]
+ openPanel.isExtensionHidden = true
+ openPanel.begin { response in
+ guard let fileURL = openPanel.url, response == .OK else {
+ #warning("logging-note: should we log something here?")
+ return
+ }
+ let libraryFile = DocumentFile(url: fileURL)
+ openWindow(value: libraryFile)
+ }
+ }
+ }
+
+ extension OpenWindowAction {
+ @MainActor public func callAsFunction(_ valueType: (some FileTypeSpecification).Type) {
+ OpenFilePanel(valueType)(with: self)
+ }
+ }
+
+#endif
diff --git a/Sources/RadiantDocs/CodablePackageDocument.swift b/Sources/RadiantDocs/CodablePackageDocument.swift
new file mode 100644
index 0000000..5337c04
--- /dev/null
+++ b/Sources/RadiantDocs/CodablePackageDocument.swift
@@ -0,0 +1,79 @@
+//
+// CodablePackageDocument.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ #if os(macOS) || os(iOS) || os(visionOS)
+ public import Foundation
+
+ public import SwiftUI
+
+ public import UniformTypeIdentifiers
+
+ public struct CodablePackageDocument: FileDocument {
+ internal enum ReadError: Error { case missingConfigurationAtKey(String) }
+
+ public static var readableContentTypes: [UTType] { T.readableContentTypes.map(UTType.init) }
+
+ let configuration: T
+
+ public init(configuration: T) { self.configuration = configuration }
+
+ public init(configuration: ReadConfiguration) throws {
+ let regularFileContents = configuration.file.fileWrappers?[T.configurationFileWrapperKey]?
+ .regularFileContents
+ guard let configJSONWrapperData = regularFileContents else {
+ throw ReadError.missingConfigurationAtKey(T.configurationFileWrapperKey)
+ }
+
+ let configuration = try T.decoder.decode(T.self, from: configJSONWrapperData)
+ self.init(configuration: configuration)
+ }
+
+ public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
+ let rootFileWrapper =
+ configuration.existingFile ?? FileWrapper(directoryWithFileWrappers: [:])
+
+ if let oldConfigJSONWrapper = rootFileWrapper.fileWrappers?[T.configurationFileWrapperKey] {
+ rootFileWrapper.removeFileWrapper(oldConfigJSONWrapper)
+ }
+
+ let newConfigJSONData = try T.encoder.encode(self.configuration)
+ let newConfigJSONWrapper = FileWrapper(regularFileWithContents: newConfigJSONData)
+ newConfigJSONWrapper.preferredFilename = T.configurationFileWrapperKey
+ rootFileWrapper.addFileWrapper(newConfigJSONWrapper)
+
+ return rootFileWrapper
+ }
+ }
+
+ extension CodablePackageDocument where T: InitializablePackage {
+ public init() { self.init(configuration: .init()) }
+ }
+ #endif
+#endif
diff --git a/Sources/RadiantDocs/Primitives/CodablePackage.swift b/Sources/RadiantDocs/Primitives/CodablePackage.swift
new file mode 100644
index 0000000..bf8a19e
--- /dev/null
+++ b/Sources/RadiantDocs/Primitives/CodablePackage.swift
@@ -0,0 +1,44 @@
+//
+// CodablePackage.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+public import Foundation
+
+public protocol CodablePackage: Sendable, Codable {
+ static var decoder: JSONDecoder { get }
+ static var encoder: JSONEncoder { get }
+ static var configurationFileWrapperKey: String { get }
+ static var readableContentTypes: [FileType] { get }
+}
+
+extension CodablePackage {
+ public init(contentsOf url: URL) throws {
+ let data = try Data(contentsOf: url.appendingPathComponent(Self.configurationFileWrapperKey))
+ self = try Self.decoder.decode(Self.self, from: data)
+ }
+}
diff --git a/Sources/RadiantDocs/Primitives/DocumentFile.swift b/Sources/RadiantDocs/Primitives/DocumentFile.swift
new file mode 100644
index 0000000..7b56688
--- /dev/null
+++ b/Sources/RadiantDocs/Primitives/DocumentFile.swift
@@ -0,0 +1,45 @@
+//
+// DocumentFile.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+public import Foundation
+
+public struct DocumentFile: Codable, Hashable {
+ public let url: URL
+
+ public init(url: URL) { self.url = url }
+
+ public func hash(into hasher: inout Hasher) { hasher.combine(url) }
+}
+
+extension DocumentFile {
+ public static func documentFile(from url: URL) -> Self? {
+ guard url.pathExtension == FileType.fileType.fileExtension else { return nil }
+ return Self(url: url)
+ }
+}
diff --git a/Sources/RadiantDocs/Primitives/FileType.swift b/Sources/RadiantDocs/Primitives/FileType.swift
new file mode 100644
index 0000000..491c746
--- /dev/null
+++ b/Sources/RadiantDocs/Primitives/FileType.swift
@@ -0,0 +1,73 @@
+//
+// FileType.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+public struct FileType: Hashable, ExpressibleByStringLiteral, Sendable {
+ public typealias StringLiteralType = String
+
+ public let utIdentifier: String
+ public let isOwned: Bool
+ public let fileExtension: String?
+
+ public init(utIdentifier: String, isOwned: Bool, fileExtension: String? = nil) {
+ self.utIdentifier = utIdentifier
+ self.isOwned = isOwned
+ self.fileExtension = fileExtension
+ }
+
+ public init(stringLiteral utIdentifier: String) {
+ self.init(utIdentifier: utIdentifier, isOwned: false)
+ }
+
+ public static func exportedAs(_ utIdentifier: String, _ fileExtension: String) -> FileType {
+ .init(utIdentifier: utIdentifier, isOwned: true, fileExtension: fileExtension)
+ }
+}
+//
+//extension FileType {
+// public static let ipswFileExtension = "ipsw"
+// public static let iTunesIPSW: FileType = "com.apple.itunes.ipsw"
+// public static let iPhoneIPSW: FileType = "com.apple.iphone.ipsw"
+//
+// public static let virtualMachineFileExtension = "bshvm"
+// public static let restoreImageLibraryFileExtension = "bshrilib"
+//
+// public static let virtualMachine: FileType = .exportedAs(
+// "com.brightdigit.bushel-vm",
+// virtualMachineFileExtension
+// )
+//
+// public static let restoreImageLibrary: FileType = .exportedAs(
+// "com.brightdigit.bushel-rilib",
+// restoreImageLibraryFileExtension
+// )
+//
+// public static let ipswTypes = [iTunesIPSW, iPhoneIPSW]
+//}
diff --git a/Sources/RadiantDocs/Primitives/FileTypeSpecification.swift b/Sources/RadiantDocs/Primitives/FileTypeSpecification.swift
new file mode 100644
index 0000000..da08d6a
--- /dev/null
+++ b/Sources/RadiantDocs/Primitives/FileTypeSpecification.swift
@@ -0,0 +1,32 @@
+//
+// FileTypeSpecification.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+public protocol FileTypeSpecification: Sendable { static var fileType: FileType { get } }
diff --git a/Sources/RadiantDocs/Primitives/InitializableFileTypeSpecification.swift b/Sources/RadiantDocs/Primitives/InitializableFileTypeSpecification.swift
new file mode 100644
index 0000000..7948842
--- /dev/null
+++ b/Sources/RadiantDocs/Primitives/InitializableFileTypeSpecification.swift
@@ -0,0 +1,35 @@
+//
+// InitializableFileTypeSpecification.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+public import Foundation
+
+public protocol InitializableFileTypeSpecification: FileTypeSpecification {
+ associatedtype WindowValueType: Codable & Hashable
+ static func createAt(_ url: URL) throws -> WindowValueType
+}
diff --git a/Sources/RadiantDocs/Primitives/InitializablePackage.swift b/Sources/RadiantDocs/Primitives/InitializablePackage.swift
new file mode 100644
index 0000000..75da2d9
--- /dev/null
+++ b/Sources/RadiantDocs/Primitives/InitializablePackage.swift
@@ -0,0 +1,51 @@
+//
+// InitializablePackage.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+public import Foundation
+
+#if canImport(FoundationNetworking)
+ public import FoundationNetworking
+#endif
+
+public protocol InitializablePackage: CodablePackage { init() }
+
+extension InitializablePackage {
+ #warning("logging-note: let's log what is going on here")
+ #warning("Might want to add parameters for creating data and creating directory.")
+ @discardableResult public static func createAt(_ fileURL: URL, using encoder: JSONEncoder) throws
+ -> Self
+ {
+ let library = self.init()
+ try FileManager.default.createDirectory(at: fileURL, withIntermediateDirectories: false)
+ let metadataJSONPath = fileURL.appendingPathComponent(self.configurationFileWrapperKey)
+ let data = try encoder.encode(library)
+ try data.write(to: metadataJSONPath)
+ return library
+ }
+}
diff --git a/Sources/RadiantDocs/UTType.swift b/Sources/RadiantDocs/UTType.swift
new file mode 100644
index 0000000..1adc982
--- /dev/null
+++ b/Sources/RadiantDocs/UTType.swift
@@ -0,0 +1,69 @@
+//
+// UTType.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(UniformTypeIdentifiers)
+ public import UniformTypeIdentifiers
+
+ extension UTType {
+ public init(fileType: FileType) {
+ if fileType.isOwned {
+ self.init(exportedAs: fileType.utIdentifier)
+ }
+ else {
+ // swift-format-ignore: NeverForceUnwrap
+ self.init(fileType.utIdentifier)!
+ }
+ }
+
+ public static func allowedContentTypes(for fileType: FileType) -> [UTType] {
+ var types = [UTType]()
+
+ if fileType.isOwned { types.append(.init(exportedAs: fileType.utIdentifier)) }
+
+ if let fileExtensionType = fileType.fileExtension.flatMap({ UTType(filenameExtension: $0) }) {
+ types.append(fileExtensionType)
+ }
+
+ if let utIdentified = UTType(fileType.utIdentifier) { types.append(utIdentified) }
+
+ return types
+ }
+
+ public static func allowedContentTypes(for fileTypes: FileType...) -> [UTType] {
+ fileTypes.flatMap(allowedContentTypes(for:))
+ }
+ }
+
+ extension FileType {
+ @Sendable public init?(url: URL) {
+ guard let utType = UTType(filenameExtension: url.pathExtension) else { return nil }
+ self.init(stringLiteral: utType.identifier)
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/AppKit/NSWindowAdaptorModifier.swift b/Sources/RadiantKit/AppKit/NSWindowAdaptorModifier.swift
new file mode 100644
index 0000000..7a40d2b
--- /dev/null
+++ b/Sources/RadiantKit/AppKit/NSWindowAdaptorModifier.swift
@@ -0,0 +1,82 @@
+//
+// NSWindowAdaptorModifier.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+
+ #if canImport(AppKit)
+ import AppKit
+
+ fileprivate struct NSWindowAdaptorHostingView: NSViewRepresentable {
+ #warning(
+ """
+ Issue 100
+ We need a way to specific when the callback is called and whether it should be.
+ """
+ )
+ private var callback: (NSWindow?) -> Void
+
+ fileprivate init(callback: @escaping (NSWindow?) -> Void) { self.callback = callback }
+
+ fileprivate func makeNSView(context _: Self.Context) -> NSView {
+ let view = NSView()
+ DispatchQueue.main.async { [weak view] in self.callback(view?.window) }
+ view.setFrameSize(.zero)
+ view.isHidden = true
+ view.frame = CGRect.zero
+ return view
+ }
+
+ fileprivate func updateNSView(_: NSView, context _: Context) {}
+ }
+
+ fileprivate struct NSWindowAdaptorModifier: ViewModifier {
+ private var callback: (NSWindow?) -> Void
+
+ fileprivate init(callback: @escaping (NSWindow?) -> Void) { self.callback = callback }
+
+ fileprivate func body(content: Content) -> some View {
+ content.overlay(NSWindowAdaptorHostingView(callback: callback).frame(width: 0, height: 0))
+ }
+ }
+
+ extension View {
+ public func nsWindowAdaptor(_ callback: @escaping (NSWindow?) -> Void) -> some View {
+ self.modifier(NSWindowAdaptorModifier(callback: callback))
+ }
+ }
+ #else
+ extension View {
+ public func nsWindowAdaptor(_: @escaping (Any?) -> Void) -> some View { self }
+ }
+ #endif
+
+#endif
diff --git a/Sources/RadiantKit/AppKit/View+NSWindowDelegate.swift b/Sources/RadiantKit/AppKit/View+NSWindowDelegate.swift
new file mode 100644
index 0000000..9cd7b3a
--- /dev/null
+++ b/Sources/RadiantKit/AppKit/View+NSWindowDelegate.swift
@@ -0,0 +1,69 @@
+//
+// View+NSWindowDelegate.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(AppKit) && canImport(SwiftUI)
+ import AppKit
+
+ public import SwiftUI
+
+ fileprivate struct NSWindowDelegateAdaptorModifier: ViewModifier {
+ @Binding var binding: (any NSWindowDelegate)?
+ let delegate: any NSWindowDelegate
+
+ init(
+ binding: Binding<(any NSWindowDelegate)?>,
+ delegate: @autoclosure () -> any NSWindowDelegate
+ ) {
+ self._binding = binding
+ self.delegate = binding.wrappedValue ?? delegate()
+
+ #warning(
+ "Issue 100 - We can't set binding here - Modifying state during view update, this will cause undefined behavior."
+ )
+ self.binding = self.delegate
+ }
+
+ func body(content: Content) -> some View {
+ content.nsWindowAdaptor { window in
+ assert(!self.delegate.isEqual(window?.delegate))
+ assert(window != nil)
+ window?.delegate = delegate
+ }
+ }
+ }
+
+ extension View {
+ public func nsWindowDelegateAdaptor(
+ _ binding: Binding<(any NSWindowDelegate)?>,
+ _ delegate: @autoclosure () -> any NSWindowDelegate
+ ) -> some View {
+ self.modifier(NSWindowDelegateAdaptorModifier(binding: binding, delegate: delegate()))
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/Binding.swift b/Sources/RadiantKit/Binding.swift
new file mode 100644
index 0000000..4834b7b
--- /dev/null
+++ b/Sources/RadiantKit/Binding.swift
@@ -0,0 +1,47 @@
+//
+// Binding.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ import Foundation
+
+ public import SwiftUI
+
+ extension Binding {
+ @MainActor public func map(
+ to get: @escaping @Sendable (Value) -> T,
+ from set: @escaping @Sendable (T) -> Value
+ ) -> Binding {
+ .init {
+ get(self.wrappedValue)
+ } set: {
+ self.wrappedValue = set($0)
+ }
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/Color.swift b/Sources/RadiantKit/Color.swift
new file mode 100644
index 0000000..235355d
--- /dev/null
+++ b/Sources/RadiantKit/Color.swift
@@ -0,0 +1,59 @@
+//
+// Color.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+
+ extension Color {
+ public init(hex: String) {
+ let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
+ var int: UInt64 = 0
+ Scanner(string: hex).scanHexInt64(&int)
+ let alpha: UInt64
+ let red: UInt64
+ let green: UInt64
+ let blue: UInt64
+ switch hex.count { case 3: // RGB (12-bit)
+ (alpha, red, green, blue) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
+ case 6: // RGB (24-bit)
+ (alpha, red, green, blue) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
+ case 8: // ARGB (32-bit)
+ (alpha, red, green, blue) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
+ default: (alpha, red, green, blue) = (255, 0, 0, 0)
+ }
+ self.init(
+ .sRGB,
+ red: Double(red) / 255,
+ green: Double(green) / 255,
+ blue: Double(blue) / 255,
+ opacity: Double(alpha) / 255
+ )
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+AppStored.swift b/Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+AppStored.swift
new file mode 100644
index 0000000..3c31a63
--- /dev/null
+++ b/Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+AppStored.swift
@@ -0,0 +1,101 @@
+//
+// AppStorage+AppStored.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+
+ public import Foundation
+
+ public import SwiftUI
+
+ extension AppStorage {
+ public init(
+ wrappedValue: Value,
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Bool, Value == Bool {
+ self.init(wrappedValue: wrappedValue, type.key, store: store)
+ }
+
+ public init(
+ wrappedValue: Value,
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Int, Value == Int {
+ self.init(wrappedValue: wrappedValue, type.key, store: store)
+ }
+
+ public init(
+ wrappedValue: Value,
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Double, Value == Double {
+ self.init(wrappedValue: wrappedValue, type.key, store: store)
+ }
+
+ public init(
+ wrappedValue: Value,
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == String, Value == String {
+ self.init(wrappedValue: wrappedValue, type.key, store: store)
+ }
+
+ public init(
+ wrappedValue: Value,
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == URL, Value == URL {
+ self.init(wrappedValue: wrappedValue, type.key, store: store)
+ }
+
+ public init(
+ wrappedValue: Value,
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Data, Value == Data {
+ self.init(wrappedValue: wrappedValue, type.key, store: store)
+ }
+
+ public init(
+ wrappedValue: Value,
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Value, Value: RawRepresentable, Value.RawValue == Int {
+ self.init(wrappedValue: wrappedValue, type.key, store: store)
+ }
+
+ public init(
+ wrappedValue: Value,
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Value, Value: RawRepresentable, Value.RawValue == String {
+ self.init(wrappedValue: wrappedValue, type.key, store: store)
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+DefaultWrapped.swift b/Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+DefaultWrapped.swift
new file mode 100644
index 0000000..23c5ab2
--- /dev/null
+++ b/Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+DefaultWrapped.swift
@@ -0,0 +1,93 @@
+//
+// AppStorage+DefaultWrapped.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+
+ public import Foundation
+
+ public import SwiftUI
+
+ extension AppStorage {
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Bool, Value == Bool {
+ self.init(wrappedValue: AppStoredType.default, type.key, store: store)
+ }
+
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Int, Value == Int {
+ self.init(wrappedValue: AppStoredType.default, type.key, store: store)
+ }
+
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Double, Value == Double {
+ self.init(wrappedValue: AppStoredType.default, type.key, store: store)
+ }
+
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == String, Value == String {
+ self.init(wrappedValue: AppStoredType.default, type.key, store: store)
+ }
+
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == URL, Value == URL {
+ self.init(wrappedValue: AppStoredType.default, type.key, store: store)
+ }
+
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Data, Value == Data {
+ self.init(wrappedValue: AppStoredType.default, type.key, store: store)
+ }
+
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Value, Value: RawRepresentable, Value.RawValue == Int {
+ self.init(wrappedValue: AppStoredType.default, type.key, store: store)
+ }
+
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ ) where AppStoredType.Value == Value, Value: RawRepresentable, Value.RawValue == String {
+ self.init(wrappedValue: AppStoredType.default, type.key, store: store)
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+ExpressibleByNilLiteral.swift b/Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+ExpressibleByNilLiteral.swift
new file mode 100644
index 0000000..0c42266
--- /dev/null
+++ b/Sources/RadiantKit/PropertyWrappers/AppStorage/AppStorage+ExpressibleByNilLiteral.swift
@@ -0,0 +1,79 @@
+//
+// AppStorage+ExpressibleByNilLiteral.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+
+ public import Foundation
+
+ public import SwiftUI
+
+ extension AppStorage where Value: ExpressibleByNilLiteral {
+ public init(for type: AppStoredType.Type, store: UserDefaults? = nil)
+ where AppStoredType.Value == Value, Value == Bool? { self.init(type.key, store: store) }
+
+ public init(for type: AppStoredType.Type, store: UserDefaults? = nil)
+ where AppStoredType.Value == Value, Value == Int? { self.init(type.key, store: store) }
+
+ public init(for type: AppStoredType.Type, store: UserDefaults? = nil)
+ where AppStoredType.Value == Value, Value == Double? { self.init(type.key, store: store) }
+
+ public init(for type: AppStoredType.Type, store: UserDefaults? = nil)
+ where AppStoredType.Value == Value, Value == String? { self.init(type.key, store: store) }
+
+ public init(for type: AppStoredType.Type, store: UserDefaults? = nil)
+ where AppStoredType.Value == Value, Value == URL? { self.init(type.key, store: store) }
+
+ public init(for type: AppStoredType.Type, store: UserDefaults? = nil)
+ where AppStoredType.Value == Value, Value == Data? { self.init(type.key, store: store) }
+ }
+
+ extension AppStorage {
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ )
+ where
+ AppStoredType.Value == R?,
+ R: RawRepresentable,
+ Value == AppStoredType.Value,
+ R.RawValue == String
+ { self.init(type.key, store: store) }
+
+ public init(
+ for type: AppStoredType.Type,
+ store: UserDefaults? = nil
+ )
+ where
+ AppStoredType.Value == R?,
+ R: RawRepresentable,
+ Value == AppStoredType.Value,
+ R.RawValue == Int
+ { self.init(type.key, store: store) }
+ }
+#endif
diff --git a/Sources/RadiantKit/PropertyWrappers/AppStored.swift b/Sources/RadiantKit/PropertyWrappers/AppStored.swift
new file mode 100644
index 0000000..38b2863
--- /dev/null
+++ b/Sources/RadiantKit/PropertyWrappers/AppStored.swift
@@ -0,0 +1,49 @@
+//
+// AppStored.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+public protocol AppStored {
+ associatedtype Value
+ static var keyType: KeyType { get }
+ static var key: String { get }
+}
+
+extension AppStored {
+ public static var key: String {
+ switch self.keyType { case .describing: String(describing: Self.self)
+
+ case .reflecting:
+ String(reflecting: Self.self).components(separatedBy: ".").dropFirst()
+ .joined(separator: ".")
+ }
+ }
+
+ public static var keyType: KeyType { .describing }
+}
diff --git a/Sources/RadiantKit/PropertyWrappers/DefaultWrapped.swift b/Sources/RadiantKit/PropertyWrappers/DefaultWrapped.swift
new file mode 100644
index 0000000..084bb38
--- /dev/null
+++ b/Sources/RadiantKit/PropertyWrappers/DefaultWrapped.swift
@@ -0,0 +1,32 @@
+//
+// DefaultWrapped.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+public protocol DefaultWrapped: AppStored { static var `default`: Value { get } }
diff --git a/Sources/RadiantKit/PropertyWrappers/KeyType.swift b/Sources/RadiantKit/PropertyWrappers/KeyType.swift
new file mode 100644
index 0000000..4c981c1
--- /dev/null
+++ b/Sources/RadiantKit/PropertyWrappers/KeyType.swift
@@ -0,0 +1,35 @@
+//
+// KeyType.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+
+public enum KeyType: Sendable {
+ case describing
+ case reflecting
+}
diff --git a/Sources/RadiantKit/TransformedValueObject.swift b/Sources/RadiantKit/TransformedValueObject.swift
new file mode 100644
index 0000000..1371932
--- /dev/null
+++ b/Sources/RadiantKit/TransformedValueObject.swift
@@ -0,0 +1,85 @@
+//
+// TransformedValueObject.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import Foundation
+
+ public import SwiftUI
+
+ @Observable public class TransformedValueObject {
+ private let defaultTransform: (InputValue) -> OutputValue
+ private let formattable: (OutputValue) -> FormattableValue
+
+ @ObservationIgnored private var transform: ((InputValue) -> OutputValue)?
+
+ @ObservationIgnored private var bindingValue: Binding?
+
+ public var inputValue: InputValue {
+ didSet {
+ assert(self.transform != nil)
+ self.bindingValue?.wrappedValue = inputValue
+ self.outputValue = (self.transform ?? self.defaultTransform)(inputValue)
+ }
+ }
+
+ public private(set) var outputValue: OutputValue {
+ didSet { self.formattedValue = self.formattable(outputValue) }
+ }
+
+ public private(set) var formattedValue: FormattableValue
+
+ public init(
+ defaultTransform: @escaping (InputValue) -> OutputValue,
+ formattable: @escaping (OutputValue) -> FormattableValue,
+ inputValue: InputValue,
+ outputValue: OutputValue? = nil,
+ fomrattedValue: FormattableValue? = nil,
+ polynomial: ((InputValue) -> OutputValue)? = nil
+ ) {
+ self.formattable = formattable
+ self.defaultTransform = defaultTransform
+ self.transform = polynomial
+ self.inputValue = inputValue
+ let outputValue = outputValue ?? transform?(inputValue) ?? defaultTransform(inputValue)
+ self.outputValue = outputValue
+ self.formattedValue = fomrattedValue ?? self.formattable(outputValue)
+ }
+
+ public func bindTo(
+ _ bindingValue: Binding,
+ using transform: @escaping (InputValue) -> OutputValue
+ ) {
+ assert(self.bindingValue == nil)
+ assert(self.transform == nil)
+ self.transform = transform
+ self.inputValue = bindingValue.wrappedValue ?? self.inputValue
+ self.bindingValue = bindingValue
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/ViewExtensions/DefaultableViewValue.swift b/Sources/RadiantKit/ViewExtensions/DefaultableViewValue.swift
new file mode 100644
index 0000000..11c8b71
--- /dev/null
+++ b/Sources/RadiantKit/ViewExtensions/DefaultableViewValue.swift
@@ -0,0 +1,32 @@
+//
+// DefaultableViewValue.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+public protocol DefaultableViewValue: Codable, Hashable, Sendable {
+ static var `default`: Self { get }
+}
diff --git a/Sources/RadiantKit/ViewExtensions/IdentifiableView.swift b/Sources/RadiantKit/ViewExtensions/IdentifiableView.swift
new file mode 100644
index 0000000..70a0c0c
--- /dev/null
+++ b/Sources/RadiantKit/ViewExtensions/IdentifiableView.swift
@@ -0,0 +1,49 @@
+//
+// IdentifiableView.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+
+ @MainActor public struct IdentifiableView: Identifiable, View, Sendable {
+ private let content: any View
+ public let id: Int
+
+ public var body: some View { AnyView(content) }
+
+ public init(_ content: any View, id: Int) {
+ self.content = content
+ self.id = id
+ }
+
+ public init(_ content: @escaping () -> some View, id: Int) {
+ self.content = content()
+ self.id = id
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/ViewExtensions/IdentifiableViewBuilder.swift b/Sources/RadiantKit/ViewExtensions/IdentifiableViewBuilder.swift
new file mode 100644
index 0000000..c64fbf1
--- /dev/null
+++ b/Sources/RadiantKit/ViewExtensions/IdentifiableViewBuilder.swift
@@ -0,0 +1,49 @@
+//
+// IdentifiableViewBuilder.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ import Foundation
+ public import SwiftUI
+
+ @MainActor @resultBuilder public enum IdentifiableViewBuilder {
+ public static func buildPartialBlock(first: any View) -> [IdentifiableView] {
+ [IdentifiableView(first, id: 0)]
+ }
+
+ public static func buildPartialBlock(accumulated: [IdentifiableView], next: any View)
+ -> [IdentifiableView]
+ { accumulated + [IdentifiableView(next, id: accumulated.count)] }
+
+ public static func buildPartialBlock(first: IdentifiableView) -> [IdentifiableView] { [first] }
+
+ public static func buildPartialBlock(accumulated: [IdentifiableView], next: IdentifiableView)
+ -> [IdentifiableView]
+ { accumulated + [next] }
+ }
+#endif
diff --git a/Sources/RadiantKit/ViewExtensions/SingleWindowView.swift b/Sources/RadiantKit/ViewExtensions/SingleWindowView.swift
new file mode 100644
index 0000000..1cf39d0
--- /dev/null
+++ b/Sources/RadiantKit/ViewExtensions/SingleWindowView.swift
@@ -0,0 +1,62 @@
+//
+// SingleWindowView.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+
+ import Foundation
+
+ public import SwiftUI
+
+ // periphery:ignore
+ public struct SingleWindowViewValue: DefaultableViewValue {
+ public static var `default`: Self { .init() }
+
+ private init() {}
+ }
+
+ @MainActor public protocol SingleWindowView: View {
+ associatedtype Value: DefaultableViewValue = SingleWindowViewValue
+ init()
+ }
+
+ extension SingleWindowView { public init(_: Binding) { self.init() } }
+
+ #if os(macOS) || os(iOS) || os(visionOS)
+ extension WindowGroup {
+ @MainActor public init(singleOf _: V.Type)
+ where Content == PresentedWindowContent {
+ self.init { value in
+ V(value)
+ } defaultValue: {
+ V.Value.default
+ }
+ }
+ }
+ #endif
+#endif
diff --git a/Sources/RadiantKit/ViewExtensions/View+GeometryProxy.swift b/Sources/RadiantKit/ViewExtensions/View+GeometryProxy.swift
new file mode 100644
index 0000000..ad2fe67
--- /dev/null
+++ b/Sources/RadiantKit/ViewExtensions/View+GeometryProxy.swift
@@ -0,0 +1,42 @@
+//
+// View+GeometryProxy.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ import Foundation
+
+ public import SwiftUI
+
+ extension View {
+ public func onGeometry(_ action: @escaping (GeometryProxy) -> Void) -> some View {
+ self.overlay {
+ GeometryReader(content: { geometry in Color.clear.onAppear(perform: { action(geometry) }) })
+ }
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/ViewExtensions/View+Hidden.swift b/Sources/RadiantKit/ViewExtensions/View+Hidden.swift
new file mode 100644
index 0000000..bea9046
--- /dev/null
+++ b/Sources/RadiantKit/ViewExtensions/View+Hidden.swift
@@ -0,0 +1,46 @@
+//
+// View+Hidden.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+
+ public import SwiftUI
+
+ extension View {
+ public func isHidden(_ value: Bool) -> some View {
+ Group {
+ if value {
+ self.hidden()
+ }
+ else {
+ self
+ }
+ }
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/Views/Button.swift b/Sources/RadiantKit/Views/Button.swift
new file mode 100644
index 0000000..7bf607d
--- /dev/null
+++ b/Sources/RadiantKit/Views/Button.swift
@@ -0,0 +1,41 @@
+//
+// Button.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+
+ extension Button {
+ public init(_ openURL: OpenURLAction, _ url: URL, @ViewBuilder _ label: () -> Label) {
+ self.init(action: { openURL.callAsFunction(url) }, label: label)
+ }
+
+ public init(_ titleKey: LocalizedStringKey, _ openURL: OpenURLAction, _ url: URL)
+ where Label == Text { self.init(openURL, url) { Text(titleKey) } }
+ }
+#endif
diff --git a/Sources/RadiantKit/Views/GuidedLabeledContent.swift b/Sources/RadiantKit/Views/GuidedLabeledContent.swift
new file mode 100644
index 0000000..0a26e40
--- /dev/null
+++ b/Sources/RadiantKit/Views/GuidedLabeledContent.swift
@@ -0,0 +1,69 @@
+//
+// GuidedLabeledContent.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+
+ public import SwiftUI
+
+ public struct GuidedLabeledContent: View {
+ let content: () -> Content
+ let label: () -> Label
+ let description: () -> Description
+
+ public var body: some View {
+ VStack {
+ LabeledContent(content: content, label: label)
+ self.description()
+ }
+ }
+
+ public init(
+ _ content: @escaping () -> Content,
+ label: @escaping () -> Label,
+ description: @escaping () -> Description
+ ) {
+ self.content = content
+ self.label = label
+ self.description = description
+ }
+ }
+
+ extension GuidedLabeledContent where Description == GuidedLabeledContentDescriptionView {
+ public init(
+ _ content: @escaping () -> Content,
+ label: @escaping () -> Label,
+ text: @escaping () -> Text,
+ descriptionAlignment: GuidedLabeledContentDescriptionView.Alignment? = .leading
+ ) {
+ self.init(content, label: label) {
+ GuidedLabeledContentDescriptionView(alignment: descriptionAlignment, text: text)
+ }
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/Views/GuidedLabeledContentDescriptionView.swift b/Sources/RadiantKit/Views/GuidedLabeledContentDescriptionView.swift
new file mode 100644
index 0000000..06aae27
--- /dev/null
+++ b/Sources/RadiantKit/Views/GuidedLabeledContentDescriptionView.swift
@@ -0,0 +1,84 @@
+//
+// GuidedLabeledContentDescriptionView.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+
+ public struct GuidedLabeledContentDescriptionView: View {
+ public enum Alignment {
+ case leading
+ case trailing
+ }
+
+ @Environment(\.layoutDirection) var layoutDirection
+ let text: () -> Text
+ let alignment: Alignment?
+
+ var multilineTextAlignment: TextAlignment {
+ switch alignment { case .leading: .leading
+
+ case .trailing: .trailing
+
+ case nil: .center
+ }
+ }
+
+ var leftSpacer: Bool {
+ switch (alignment, layoutDirection) { case (.trailing, .leftToRight): true
+
+ case (.leading, .rightToLeft): true
+
+ default: false
+ }
+ }
+
+ var rightSpacer: Bool {
+ switch (alignment, layoutDirection) { case (.leading, .leftToRight): true
+
+ case (.trailing, .rightToLeft): true
+
+ default: false
+ }
+ }
+
+ public var body: some View {
+ HStack {
+ if leftSpacer { Spacer() }
+ text().font(.callout).multilineTextAlignment(self.multilineTextAlignment)
+
+ if rightSpacer { Spacer() }
+ }
+ }
+
+ internal init(alignment: Alignment? = nil, text: @escaping () -> Text) {
+ self.text = text
+ self.alignment = alignment
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/Views/PreferredLayoutView.swift b/Sources/RadiantKit/Views/PreferredLayoutView.swift
new file mode 100644
index 0000000..a0a9f74
--- /dev/null
+++ b/Sources/RadiantKit/Views/PreferredLayoutView.swift
@@ -0,0 +1,100 @@
+//
+// PreferredLayoutView.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ import Foundation
+
+ public import SwiftUI
+
+ public enum PreferredLayout {
+ public struct Value {
+ private let value: Value?
+
+ fileprivate let update: (Value) -> Void
+
+ fileprivate init(value: Value?, update: @escaping (Value) -> Void) {
+ self.value = value
+ self.update = update
+ }
+
+ public func get() -> Value? { value }
+ }
+
+ public struct View: SwiftUI.View {
+ let reduce: (ValueType, ValueType) -> ValueType
+ let content: (Value) -> Content
+ @State private var value: ValueType?
+
+ public var body: some SwiftUI.View {
+ let valueType = Value(value: self.value) { newValue in
+ let value = value ?? newValue
+ self.value = reduce(value, newValue)
+ }
+ content(valueType)
+ }
+
+ public init(
+ initial: ValueType? = nil,
+ reduce: @escaping (ValueType, ValueType) -> ValueType,
+ content: @escaping (Value) -> Content
+ ) {
+ self.reduce = reduce
+ self.content = content
+ self.value = initial
+ }
+ }
+ }
+ @available(*, deprecated, renamed: "PreferredLayout.View") public typealias PreferredLayoutView =
+ PreferredLayout.View
+
+ extension PreferredLayout.View where ValueType: Comparable {
+ public init(content: @escaping (PreferredLayout.Value) -> Content) {
+ self.init(reduce: max, content: content)
+ }
+ }
+
+ extension View {
+ public func apply(
+ _ keyPath: KeyPath,
+ with valueType: PreferredLayout.Value
+ ) -> some View { self.apply(keyPath, with: valueType) { Color.clear } }
+
+ public func apply(
+ _ keyPath: KeyPath,
+ with valueType: PreferredLayout.Value,
+ backgroundView: @escaping () -> some View
+ ) -> some View {
+ self.background(
+ GeometryReader { geometry in
+ backgroundView().onAppear(perform: { valueType.update(geometry[keyPath: keyPath]) })
+ }
+ )
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/Views/SliderStepperView.swift b/Sources/RadiantKit/Views/SliderStepperView.swift
new file mode 100644
index 0000000..b191016
--- /dev/null
+++ b/Sources/RadiantKit/Views/SliderStepperView.swift
@@ -0,0 +1,72 @@
+//
+// SliderStepperView.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+
+ @MainActor public struct SliderStepperView: View {
+ private let title: TitleType
+ private let label: @Sendable (TitleType) -> Label
+ private let bounds: ClosedRange
+ private let step: Float
+ private let content: @Sendable @MainActor (TitleType) -> Content
+ @Binding private var value: Float
+
+ public var body: some View {
+ LabeledContent {
+ HStack {
+ Slider(value: $value, in: bounds, step: step)
+ self.content(self.title).labelsHidden().frame(width: 50).padding(.horizontal, 6.0)
+
+ Stepper(value: $value, in: bounds, step: 1.0, label: { self.label(self.title) })
+ .labelsHidden()
+ }
+ } label: {
+ self.label(self.title)
+ }
+ }
+
+ public init(
+ title: TitleType,
+ label: @escaping @Sendable (TitleType) -> Label,
+ value: Binding,
+ bounds: ClosedRange,
+ step: Float,
+ content: @escaping @MainActor @Sendable (TitleType) -> Content
+ ) {
+ self.title = title
+ self.label = label
+ self._value = value
+ self.bounds = bounds
+ self.step = step
+ self.content = content
+ }
+ }
+
+#endif
diff --git a/Sources/RadiantKit/Views/ValueTextBubble.swift b/Sources/RadiantKit/Views/ValueTextBubble.swift
new file mode 100644
index 0000000..f929788
--- /dev/null
+++ b/Sources/RadiantKit/Views/ValueTextBubble.swift
@@ -0,0 +1,90 @@
+//
+// ValueTextBubble.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import Foundation
+
+ public import SwiftUI
+
+ public struct ValueTextBubble<
+ ShapeStyleType: ShapeStyle,
+ ValueType: Equatable,
+ FormatStyleType: FormatStyle
+ >: View where FormatStyleType.FormatInput == ValueType, FormatStyleType.FormatOutput == String {
+ public let value: ValueType
+ public let format: FormatStyleType
+ public let backgroundStyle: ShapeStyleType
+ public let cornerRadius: CGFloat
+
+ public var body: some View {
+ Text(value, format: format)
+ .background {
+ RoundedRectangle(cornerRadius: cornerRadius).padding(-2).padding(.horizontal, -4)
+ .foregroundStyle(backgroundStyle)
+ }
+ .padding(.horizontal, 2)
+ }
+
+ public init(
+ value: ValueType,
+ format: FormatStyleType,
+ backgroundStyle: ShapeStyleType,
+ cornerRadius: CGFloat = 18
+ ) {
+ self.value = value
+ self.format = format
+ self.cornerRadius = cornerRadius
+ self.backgroundStyle = backgroundStyle
+ }
+
+ public init(
+ value: ValueType,
+ format: FormatStyleType,
+ backgroundColor: Color = Color.primary.opacity(0.25),
+ cornerRadius: CGFloat = 18
+ ) where ShapeStyleType == Color {
+ self.value = value
+ self.format = format
+ self.cornerRadius = cornerRadius
+ self.backgroundStyle = backgroundColor
+ }
+
+ public init(
+ value: Int,
+ format: IntegerFormatStyle = FormatStyleType.number,
+ backgroundColor: Color = Color.primary.opacity(0.25),
+ cornerRadius: CGFloat = 18
+ ) where ValueType == Int, ShapeStyleType == Color, FormatStyleType == IntegerFormatStyle {
+ self.value = value
+ self.format = format
+ self.cornerRadius = cornerRadius
+ self.backgroundStyle = backgroundColor
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/Views/VerticalLabelStyle.swift b/Sources/RadiantKit/Views/VerticalLabelStyle.swift
new file mode 100644
index 0000000..1df8cef
--- /dev/null
+++ b/Sources/RadiantKit/Views/VerticalLabelStyle.swift
@@ -0,0 +1,69 @@
+//
+// VerticalLabelStyle.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+
+ public struct VerticalLabelStyle: LabeledContentStyle {
+ let alignment: HorizontalAlignment
+ let labelFont: Font
+ let labelPaddingEdgeInsets: EdgeInsets
+
+ public init(
+ alignment: HorizontalAlignment = .leading,
+ labelFont: Font = .subheadline,
+ labelPaddingEdgeInsets: EdgeInsets = .init(top: 0, leading: 4.0, bottom: 0.0, trailing: 0.0)
+ ) {
+ self.alignment = alignment
+ self.labelFont = labelFont
+ self.labelPaddingEdgeInsets = labelPaddingEdgeInsets
+ }
+
+ public func makeBody(configuration: Configuration) -> some View {
+ VStack(alignment: alignment) {
+ configuration.content.labelsHidden()
+ configuration.label.font(labelFont).padding(labelPaddingEdgeInsets)
+ }
+ }
+ }
+
+ extension LabeledContentStyle {
+ public static func vertical(
+ alignment: HorizontalAlignment = .leading,
+ labelFont: Font = .subheadline,
+ labelPaddingEdgeInsets: EdgeInsets = .init(top: 0, leading: 2.0, bottom: 0.0, trailing: 0.0)
+ ) -> Self where Self == VerticalLabelStyle {
+ .init(
+ alignment: alignment,
+ labelFont: labelFont,
+ labelPaddingEdgeInsets: labelPaddingEdgeInsets
+ )
+ }
+ }
+#endif
diff --git a/Sources/RadiantKit/Views/Video.swift b/Sources/RadiantKit/Views/Video.swift
new file mode 100644
index 0000000..a2d3d3d
--- /dev/null
+++ b/Sources/RadiantKit/Views/Video.swift
@@ -0,0 +1,53 @@
+//
+// Video.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI) && canImport(AppKit)
+ public import AVKit
+
+ public import SwiftUI
+
+ public struct Video: NSViewRepresentable {
+ let player: AVPlayer?
+
+ public init(using player: AVPlayer!) {
+ assert(player != nil)
+ self.player = player
+ }
+
+ public func makeNSView(context _: Context) -> AVPlayerView {
+ let view = AVPlayerView()
+ view.controlsStyle = .none
+ view.player = player
+ player?.play()
+ return view
+ }
+
+ public func updateNSView(_: AVPlayerView, context _: Context) {}
+ }
+#endif
diff --git a/Sources/RadiantPaging/CancelPageAction.swift b/Sources/RadiantPaging/CancelPageAction.swift
new file mode 100644
index 0000000..d634acc
--- /dev/null
+++ b/Sources/RadiantPaging/CancelPageAction.swift
@@ -0,0 +1,46 @@
+//
+// CancelPageAction.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ import Foundation
+ public import SwiftUI
+
+ fileprivate struct CancelPageKey: EnvironmentKey, Sendable {
+ static let defaultValue: CancelPageAction = .default
+ }
+
+ public typealias CancelPageAction = PageAction
+
+ extension EnvironmentValues {
+ public var cancelPage: CancelPageAction {
+ get { self[CancelPageKey.self] }
+ set { self[CancelPageKey.self] = newValue }
+ }
+ }
+#endif
diff --git a/Sources/RadiantPaging/ContainerView.swift b/Sources/RadiantPaging/ContainerView.swift
new file mode 100644
index 0000000..355d866
--- /dev/null
+++ b/Sources/RadiantPaging/ContainerView.swift
@@ -0,0 +1,100 @@
+//
+// ContainerView.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+
+ public struct ContainerView: View {
+ @Environment(\.pageNavigationAvailability) private var pageNavigationAvailability
+ @Environment(\.previousPage) private var previousPage
+ @Environment(\.nextPage) private var nextPage
+ @Environment(\.cancelPage) private var cancel
+ private let content: (Binding) -> Content
+ private let label: () -> Label
+ @State private var isNextReady = false
+
+ private var isPreviousDisabled: Bool { !pageNavigationAvailability.contains(.previous) }
+
+ private var isNextDisabled: Bool { !isNextReady }
+
+ public var body: some View {
+ VStack {
+ HStack {
+ label()
+ Spacer()
+ }
+ Spacer()
+ HStack {
+ Spacer()
+ VStack {
+ Spacer()
+ content($isNextReady).padding()
+ Spacer()
+ }
+ Spacer()
+ }
+ .border(Color.primary.opacity(0.2))
+ Spacer().frame(height: 16)
+ HStack {
+ Button {
+ cancel()
+ } label: {
+ Text("Cancel").padding(.horizontal)
+ }
+ Spacer()
+ Button {
+ previousPage()
+ } label: {
+ Text("Previous").padding(.horizontal)
+ }
+ .disabled(isPreviousDisabled).opacity(isPreviousDisabled ? 0.8 : 1.0)
+ Button {
+ nextPage()
+ } label: {
+ Text("Next").padding(.horizontal)
+ }
+ .disabled(isNextDisabled).opacity(isNextDisabled ? 0.8 : 1.0)
+ }
+ }
+ .padding()
+ }
+
+ public init(label: @escaping () -> Label, content: @escaping (Binding) -> Content) {
+ self.label = label
+ self.content = content
+ }
+ }
+
+ #Preview {
+ ContainerView(
+ label: { Text("Hello World") },
+ content: { _ in Form { Section { Text("Hello World") } } }
+ )
+ }
+#endif
diff --git a/Sources/RadiantPaging/DismissParameters.swift b/Sources/RadiantPaging/DismissParameters.swift
new file mode 100644
index 0000000..df9ffcc
--- /dev/null
+++ b/Sources/RadiantPaging/DismissParameters.swift
@@ -0,0 +1,48 @@
+//
+// DismissParameters.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+import Foundation
+public import RadiantKit
+
+public struct DismissParameters {
+ #if canImport(SwiftUI)
+ public typealias PageID = IdentifiableView.ID
+ #else
+ public typealias PageID = Int
+ #endif
+ public enum Action {
+ case previous
+ case next
+ case cancel
+ }
+
+ public let currentPageIndex: Int
+ public let currentPageID: PageID?
+ public let action: Action
+}
diff --git a/Sources/RadiantPaging/NextPageAction.swift b/Sources/RadiantPaging/NextPageAction.swift
new file mode 100644
index 0000000..70569ef
--- /dev/null
+++ b/Sources/RadiantPaging/NextPageAction.swift
@@ -0,0 +1,57 @@
+//
+// NextPageAction.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ import Foundation
+ public import SwiftUI
+
+ fileprivate struct NextPageKey: EnvironmentKey, Sendable {
+ fileprivate static let defaultValue: NextPageAction = .default
+ }
+
+ public struct PageAction: Sendable {
+ internal static let `default`: PageAction = .init { assertionFailure() }
+
+ private let pageFunction: @Sendable @MainActor () -> Void
+ internal init(_ pageFunction: @Sendable @MainActor @escaping () -> Void) {
+ self.pageFunction = pageFunction
+ }
+
+ @MainActor public func callAsFunction() { pageFunction() }
+ }
+
+ public typealias NextPageAction = PageAction
+
+ extension EnvironmentValues {
+ public var nextPage: NextPageAction {
+ get { self[NextPageKey.self] }
+ set { self[NextPageKey.self] = newValue }
+ }
+ }
+#endif
diff --git a/Sources/RadiantPaging/PageNavigationAvailability.swift b/Sources/RadiantPaging/PageNavigationAvailability.swift
new file mode 100644
index 0000000..00bf5d0
--- /dev/null
+++ b/Sources/RadiantPaging/PageNavigationAvailability.swift
@@ -0,0 +1,61 @@
+//
+// PageNavigationAvailability.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ import Foundation
+ public import SwiftUI
+
+ fileprivate struct PageNavigationAvailabilityKey: EnvironmentKey, Sendable {
+ static let defaultValue: PageNavigationAvailability = .default
+ }
+
+ public struct PageNavigationAvailability: OptionSet, Sendable {
+ public typealias RawValue = Int
+ public static let `default`: PageNavigationAvailability = .none
+
+ public static let none: PageNavigationAvailability = .init()
+ public static let next: PageNavigationAvailability = .init(rawValue: 1)
+ public static let previous: PageNavigationAvailability = .init(rawValue: 2)
+ public static let both: PageNavigationAvailability = [previous, next]
+
+ public let rawValue: Int
+
+ public init(rawValue: RawValue) {
+ assert(rawValue < 4)
+ self.rawValue = rawValue
+ }
+ }
+
+ extension EnvironmentValues {
+ public var pageNavigationAvailability: PageNavigationAvailability {
+ get { self[PageNavigationAvailabilityKey.self] }
+ set { self[PageNavigationAvailabilityKey.self] = newValue }
+ }
+ }
+#endif
diff --git a/Sources/RadiantPaging/PageView.swift b/Sources/RadiantPaging/PageView.swift
new file mode 100644
index 0000000..1d986be
--- /dev/null
+++ b/Sources/RadiantPaging/PageView.swift
@@ -0,0 +1,138 @@
+//
+// PageView.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+ public import RadiantKit
+
+ @MainActor public struct PageView: View, Sendable {
+ @Environment(\.dismiss) private var dismiss
+ @State private var currentPageID: IdentifiableView.ID?
+
+ private let onDimiss: (@MainActor @Sendable (DismissParameters) -> Void)?
+ private let pages: [IdentifiableView]
+
+ public var pageNavigationAvailability: PageNavigationAvailability {
+ switch (currentPageID, currentPageID == pages.first?.id, currentPageID == pages.last?.id) {
+ case (.none, _, _), (.some, true, true): .none
+
+ case (.some, false, false): .both
+
+ case (.some, false, true): .previous
+
+ case (.some, true, false): .next
+ }
+ }
+
+ public var body: some View {
+ ForEach(pages) { page in
+ if page.id == currentPageID {
+ AnyView(
+ page.environment(\.pageNavigationAvailability, pageNavigationAvailability)
+ .environment(\.nextPage, NextPageAction { showNextPage() })
+ .environment(\.previousPage, PreviousPageAction { showPreviousPage() })
+ .environment(\.cancelPage, CancelPageAction { cancelPage() })
+ )
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ else if currentPageID == nil, !pages.isEmpty {
+ Color.clear.onAppear { currentPageID = pages.first?.id }
+ }
+ }
+ }
+
+ public init(
+ onDismiss: (@MainActor @Sendable (DismissParameters) -> Void)? = nil,
+ @IdentifiableViewBuilder _ pagesBuilder: () -> [IdentifiableView]
+ ) { self.init(pages: pagesBuilder(), onDismiss: onDismiss) }
+
+ public init(
+ pages: [IdentifiableView],
+ onDismiss: (@Sendable @MainActor (DismissParameters) -> Void)?
+ ) {
+ self.pages = pages
+ onDimiss = onDismiss
+
+ _currentPageID = State(initialValue: pages.first?.id)
+ }
+
+ private func cancelPage() {
+ guard let currentIndex = pages.firstIndex(where: { $0.id == self.currentPageID }) else {
+ return
+ }
+ onDimiss?(
+ .init(currentPageIndex: currentIndex, currentPageID: currentPageID, action: .cancel)
+ )
+ }
+
+ private func showNextPage() {
+ guard let currentIndex = pages.firstIndex(where: { $0.id == self.currentPageID }) else {
+ return
+ }
+ let nextIndex = currentIndex + 1
+ guard nextIndex < pages.count else {
+ assert(pages.count == nextIndex)
+
+ if pages.count != nextIndex {
+ // Self.logger.error("Invalid page index \(nextIndex) > \(pages.count)")
+ }
+
+ onDimiss?(
+ .init(currentPageIndex: currentIndex, currentPageID: currentPageID, action: .next)
+ )
+ // dismiss()
+
+ return
+ }
+ currentPageID = pages[currentIndex + 1].id
+ }
+
+ private func showPreviousPage() {
+ guard let currentIndex = pages.firstIndex(where: { $0.id == self.currentPageID }) else {
+ return
+ }
+ let previousIndex = currentIndex - 1
+ guard previousIndex >= 0 else {
+ assert(previousIndex == 0)
+
+ if previousIndex != 0 {
+ // Self.logger.error("Invalid page index \(nextIndex) > \(pages.count)")
+ }
+
+ onDimiss?(
+ .init(currentPageIndex: currentIndex, currentPageID: currentPageID, action: .previous)
+ )
+ dismiss()
+
+ return
+ }
+ currentPageID = pages[currentIndex - 1].id
+ }
+ }
+#endif
diff --git a/Sources/RadiantPaging/PreviousPageAction.swift b/Sources/RadiantPaging/PreviousPageAction.swift
new file mode 100644
index 0000000..2a8afdb
--- /dev/null
+++ b/Sources/RadiantPaging/PreviousPageAction.swift
@@ -0,0 +1,46 @@
+//
+// PreviousPageAction.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ import Foundation
+ public import SwiftUI
+
+ fileprivate struct PreviousPageKey: EnvironmentKey, Sendable {
+ static let defaultValue: PreviousPageAction = .default
+ }
+
+ public typealias PreviousPageAction = PageAction
+
+ extension EnvironmentValues {
+ public var previousPage: PreviousPageAction {
+ get { self[PreviousPageKey.self] }
+ set { self[PreviousPageKey.self] = newValue }
+ }
+ }
+#endif
diff --git a/Sources/RadiantProgress/CopyOperation.swift b/Sources/RadiantProgress/CopyOperation.swift
new file mode 100644
index 0000000..712c1ac
--- /dev/null
+++ b/Sources/RadiantProgress/CopyOperation.swift
@@ -0,0 +1,139 @@
+//
+// CopyOperation.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Observation)
+ public import Foundation
+ public import Observation
+
+ #if canImport(OSLog)
+ public import OSLog
+ #else
+ public import Logging
+ #endif
+
+ @MainActor @Observable
+ public final class CopyOperation: Identifiable {
+ private let sourceURL: URL
+ private let destinationURL: URL
+ public let totalValue: ValueType?
+ private let timeInterval: TimeInterval
+ private nonisolated let getSize: @Sendable (URL) throws -> ValueType?
+ private let copyFile: @Sendable (CopyPaths) async throws -> Void
+ public var currentValue = ValueType.zero
+ private var timer: Timer?
+ private let logger: Logger?
+
+ public nonisolated var id: URL { sourceURL }
+
+ public init(
+ sourceURL: URL,
+ destinationURL: URL,
+ totalValue: ValueType?,
+ timeInterval: TimeInterval,
+ logger: Logger?,
+ getSize: @escaping @Sendable (URL) throws -> ValueType?,
+ copyFile: @escaping @Sendable (CopyPaths) async throws -> Void
+ ) {
+ self.sourceURL = sourceURL
+ self.destinationURL = destinationURL
+ self.totalValue = totalValue
+ self.timeInterval = timeInterval
+ self.getSize = getSize
+ self.copyFile = copyFile
+ self.logger = logger
+ }
+
+ private nonisolated func updateValue(_ currentValue: ValueType) {
+ Task { await self.updatingValue(currentValue) }
+ }
+
+ private func updatingValue(_ currentValue: ValueType) {
+ self.currentValue = currentValue
+ if let totalValue = self.totalValue {
+ guard self.currentValue < totalValue else {
+ self.logger?.debug("Copy is complete based on size. Quitting timer.")
+ self.killTimer()
+ return
+ }
+ }
+ else {
+ self.logger?.warning("Total size is missing")
+ }
+ }
+
+ private func starTimer() {
+ timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) {
+ [weak self] timer in
+ guard let weakSelf = self else {
+ timer.invalidate()
+ return
+ }
+ weakSelf.logger?.debug("Timer On")
+ assert(weakSelf.logger != nil)
+ Task {
+ let currentValue: ValueType?
+ do { currentValue = try weakSelf.getSize(weakSelf.destinationURL) }
+ catch {
+ weakSelf.logger?.error("Unable to get size: \(error)")
+ assertionFailure("Unable to get size: \(error)")
+ currentValue = nil
+ }
+ if let currentValue {
+ weakSelf.updateValue(currentValue)
+ weakSelf.logger?.debug("Updating size to: \(currentValue)")
+ }
+ else {
+ weakSelf.logger?.warning("Unable to get size")
+ }
+ }
+ }
+ }
+
+ public func execute() async throws {
+ self.logger?.debug("Starting Copy operating")
+ await starTimer()
+ do { try await self.copyFile(.init(fromURL: sourceURL, toURL: destinationURL)) }
+ catch {
+ self.logger?.error("Error Copying: \(error)")
+ self.killTimer()
+ throw error
+ }
+ self.logger?.debug("Copy is done. Quitting timer.")
+ self.killTimer()
+ }
+
+ private func killTimer() {
+ timer?.invalidate()
+ timer = nil
+ }
+ }
+
+ extension CopyOperation: ProgressOperation {}
+
+#endif
diff --git a/Sources/RadiantProgress/CopyPaths.swift b/Sources/RadiantProgress/CopyPaths.swift
new file mode 100644
index 0000000..b434952
--- /dev/null
+++ b/Sources/RadiantProgress/CopyPaths.swift
@@ -0,0 +1,35 @@
+//
+// CopyPaths.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+public import Foundation
+
+public struct CopyPaths {
+ public let fromURL: URL
+ public let toURL: URL
+}
diff --git a/Sources/RadiantProgress/DownloadOperation.swift b/Sources/RadiantProgress/DownloadOperation.swift
new file mode 100644
index 0000000..02f0ee3
--- /dev/null
+++ b/Sources/RadiantProgress/DownloadOperation.swift
@@ -0,0 +1,87 @@
+//
+// DownloadOperation.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Observation)
+ public import Foundation
+ public import Observation
+
+ #if canImport(FoundationNetworking)
+ public import FoundationNetworking
+ #endif
+
+ @MainActor @Observable
+ public final class DownloadOperation:
+
+ Identifiable, ProgressOperation, Sendable
+ {
+ private let download: Downloader
+ private let sourceURL: URL
+ private let destinationURL: URL
+
+ public nonisolated var id: URL { sourceURL }
+
+ public var currentValue: ValueType { .init(download.totalBytesWritten) }
+
+ public var totalValue: ValueType? { download.totalBytesExpectedToWrite.map(ValueType.init(_:)) }
+
+ #if canImport(Combine)
+ public init(
+ sourceURL: URL,
+ destinationURL: URL,
+ totalBytesExpectedToWrite: ValueType?,
+ configuration: URLSessionConfiguration? = nil,
+ queue: OperationQueue? = nil
+ ) {
+ assert(!sourceURL.isFileURL)
+ assert(totalBytesExpectedToWrite != nil)
+ self.sourceURL = sourceURL
+ self.destinationURL = destinationURL
+ self.download = ObservableDownloader(
+ totalBytesExpectedToWrite: totalBytesExpectedToWrite,
+ configuration: configuration,
+ queue: queue
+ )
+ }
+ #endif
+ public init(sourceURL: URL, destinationURL: URL, download: Downloader) {
+ self.download = download
+ self.sourceURL = sourceURL
+ self.destinationURL = destinationURL
+ }
+
+ public func execute() async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ self.download.begin(from: self.sourceURL, to: self.destinationURL) { result in
+ continuation.resume(with: result)
+ }
+ }
+ }
+ }
+
+#endif
diff --git a/Sources/RadiantProgress/DownloadUpdate.swift b/Sources/RadiantProgress/DownloadUpdate.swift
new file mode 100644
index 0000000..4bbec4b
--- /dev/null
+++ b/Sources/RadiantProgress/DownloadUpdate.swift
@@ -0,0 +1,33 @@
+//
+// DownloadUpdate.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+internal struct DownloadUpdate: Sendable {
+ internal let totalBytesWritten: Int64
+ internal let totalBytesExpectedToWrite: Int64?
+}
diff --git a/Sources/RadiantProgress/Downloader.swift b/Sources/RadiantProgress/Downloader.swift
new file mode 100644
index 0000000..f8c83bb
--- /dev/null
+++ b/Sources/RadiantProgress/Downloader.swift
@@ -0,0 +1,44 @@
+//
+// Downloader.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+public import Foundation
+
+#if canImport(FoundationNetworking)
+ public import FoundationNetworking
+#endif
+
+@MainActor public protocol Downloader {
+ var totalBytesWritten: Int64 { get }
+ var totalBytesExpectedToWrite: Int64? { get }
+ func begin(
+ from downloadSourceURL: URL,
+ to destinationFileURL: URL,
+ _ completion: @escaping @Sendable (Result) -> Void
+ )
+}
diff --git a/Sources/RadiantProgress/FileOperationProgress.swift b/Sources/RadiantProgress/FileOperationProgress.swift
new file mode 100644
index 0000000..e0993da
--- /dev/null
+++ b/Sources/RadiantProgress/FileOperationProgress.swift
@@ -0,0 +1,50 @@
+//
+// FileOperationProgress.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Observation)
+ public import Foundation
+ public import Observation
+
+ @MainActor @Observable
+ public final class FileOperationProgress: Identifiable {
+ public let operation: any ProgressOperation
+
+ public nonisolated var id: URL { operation.id }
+
+ public var totalValueBytes: Int64? { operation.totalValue.map(Int64.init) }
+
+ public var currentValueBytes: Int64 { Int64(operation.currentValue) }
+
+ internal var currentValue: Double { Double(operation.currentValue) }
+
+ internal var totalValue: Double? { operation.totalValue.map(Double.init) }
+
+ public init(_ operation: any ProgressOperation) { self.operation = operation }
+ }
+#endif
diff --git a/Sources/RadiantProgress/ObservableDownloader.swift b/Sources/RadiantProgress/ObservableDownloader.swift
new file mode 100644
index 0000000..c3e99f5
--- /dev/null
+++ b/Sources/RadiantProgress/ObservableDownloader.swift
@@ -0,0 +1,234 @@
+//
+// ObservableDownloader.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Combine) && canImport(Observation)
+ public import Combine
+
+ public import Foundation
+
+ fileprivate protocol DownloadObserver {
+ func finishedDownloadingTo(_ location: URL)
+ func progressUpdated(_ progress: DownloadUpdate)
+ func didComplete(withError error: Error?)
+ }
+
+ private actor ObserverContainer: Sendable {
+ private nonisolated(unsafe) var observer: DownloadObserver?
+
+ nonisolated func setObserver(_ observer: DownloadObserver) {
+ assert(self.observer == nil)
+ self.observer = observer
+ }
+
+ nonisolated func on(_ closure: @escaping @Sendable (DownloadObserver) -> Void) {
+ Task { await self.withObserver(closure) }
+ }
+
+ private func withObserver(_ closure: @escaping @Sendable (DownloadObserver) -> Void) {
+ assert(self.observer != nil)
+ guard let observer else { return }
+
+ closure(observer)
+ }
+ }
+
+ fileprivate final class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
+ let container = ObserverContainer()
+
+ func setObserver(_ observer: DownloadObserver) { self.container.setObserver(observer) }
+
+ func urlSession(
+ _: URLSession,
+ downloadTask _: URLSessionDownloadTask,
+ didFinishDownloadingTo location: URL
+ ) {
+ let newLocation = FileManager.default.temporaryDirectory
+ .appendingPathComponent(UUID().uuidString).appendingPathExtension(location.pathExtension)
+ do { try FileManager.default.copyItem(at: location, to: newLocation) }
+ catch {
+ container.on { observer in observer.didComplete(withError: error) }
+ return
+ }
+ container.on { observer in observer.finishedDownloadingTo(newLocation) }
+ }
+
+ func urlSession(
+ _: URLSession,
+ downloadTask _: URLSessionDownloadTask,
+ didWriteData _: Int64,
+ totalBytesWritten: Int64,
+ totalBytesExpectedToWrite: Int64
+ ) {
+ container.on { observer in
+ observer.progressUpdated(
+ .init(
+ totalBytesWritten: totalBytesWritten,
+ totalBytesExpectedToWrite: totalBytesExpectedToWrite
+ )
+ )
+ }
+ }
+
+ func urlSession(_: URLSession, task _: URLSessionTask, didCompleteWithError error: (any Error)?)
+ { container.on { observer in observer.didComplete(withError: error) } }
+ }
+
+ @Observable @MainActor public final class ObservableDownloader: DownloadObserver, Downloader {
+ internal struct DownloadRequest {
+ internal let downloadSourceURL: URL
+ internal let destinationFileURL: URL
+ }
+
+ // swift-format-ignore: NeverUseImplicitlyUnwrappedOptionals
+ @ObservationIgnored private var delegate: DownloadDelegate!
+
+ public internal(set) var totalBytesWritten: Int64 = 0
+ public internal(set) var totalBytesExpectedToWrite: Int64?
+
+ // swift-format-ignore: NeverUseImplicitlyUnwrappedOptionals
+ @ObservationIgnored internal private(set) var session: URLSession!
+
+ internal let resumeDataSubject = PassthroughSubject()
+ internal var task: URLSessionDownloadTask?
+
+ private var cancellables = [AnyCancellable]()
+ internal let requestSubject = PassthroughSubject()
+ internal let locationURLSubject = PassthroughSubject()
+ internal let downloadUpdate = PassthroughSubject()
+ private var completion: ((Result) -> Void)?
+
+ private let formatter = ByteCountFormatter()
+
+ public var prettyBytesWritten: String {
+ formatter.string(from: .init(value: .init(totalBytesWritten), unit: .bytes))
+ }
+
+ public convenience init(
+ totalBytesExpectedToWrite: (some BinaryInteger)?,
+ configuration: URLSessionConfiguration? = nil,
+ queue: OperationQueue? = nil
+ ) {
+ self.init(
+ totalBytesExpectedToWrite: totalBytesExpectedToWrite,
+ setupPublishers: .init(),
+ configuration: configuration,
+ queue: queue
+ )
+ }
+ internal convenience init(
+ totalBytesExpectedToWrite: (some BinaryInteger)?,
+ setupPublishers: SetupPublishers,
+ configuration: URLSessionConfiguration? = nil,
+ queue: OperationQueue? = nil
+ ) {
+ self.init(
+ totalBytesExpectedToWrite: totalBytesExpectedToWrite,
+ setupPublishers: setupPublishers.callAsFunction(downloader:),
+ configuration: configuration,
+ queue: queue
+ )
+ }
+
+ public init(
+ totalBytesExpectedToWrite: (some BinaryInteger)?,
+ setupPublishers: @escaping (ObservableDownloader) -> [AnyCancellable],
+ configuration: URLSessionConfiguration? = nil,
+ queue: OperationQueue? = nil
+ ) {
+ self.totalBytesExpectedToWrite = totalBytesExpectedToWrite.map(Int64.init(_:))
+
+ let delegate = DownloadDelegate()
+ self.session = URLSession(
+ configuration: configuration ?? .default,
+ delegate: delegate,
+ delegateQueue: queue
+ )
+
+ self.delegate = delegate
+
+ self.cancellables = setupPublishers(self)
+ }
+
+ func onCompletion(_ result: Result) {
+ assert(completion != nil)
+ completion?(result)
+ }
+
+ public func cancel() { task?.cancel() }
+
+ public func begin(
+ from downloadSourceURL: URL,
+ to destinationFileURL: URL,
+ _ completion: @escaping @Sendable (Result) -> Void
+ ) {
+ #warning("Requires Testing")
+ self.delegate.setObserver(self)
+ assert(self.completion == nil)
+ self.completion = completion
+ requestSubject.send(
+ .init(downloadSourceURL: downloadSourceURL, destinationFileURL: destinationFileURL)
+ )
+ }
+
+ nonisolated func finishedDownloadingTo(_ location: URL) {
+ Task { @MainActor in self.finishedDownloadingToAsync(location) }
+ }
+
+ nonisolated func progressUpdated(_ progress: DownloadUpdate) {
+ Task { @MainActor in self.progressUpdatedAsync(progress) }
+ }
+
+ nonisolated func didComplete(withError error: (any Error)?) {
+ Task { @MainActor in self.didCompleteAsync(withError: error) }
+ }
+
+ func finishedDownloadingToAsync(_ location: URL) { locationURLSubject.send(location) }
+
+ func progressUpdatedAsync(_ progress: DownloadUpdate) { self.downloadUpdate.send(progress) }
+
+ func didCompleteAsync(withError error: (any Error)?) {
+ guard let error else {
+ // Handle success case.
+ return
+ }
+ let userInfo = (error as NSError).userInfo
+ if let resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
+ resumeDataSubject.send(resumeData)
+ }
+ }
+ deinit {
+ MainActor.assumeIsolated {
+ for cancellable in self.cancellables { cancellable.cancel() }
+ self.cancellables.removeAll()
+ self.session = nil
+ self.task = nil
+ }
+ }
+ }
+#endif
diff --git a/Sources/RadiantProgress/PreviewOperation.swift b/Sources/RadiantProgress/PreviewOperation.swift
new file mode 100644
index 0000000..34be65c
--- /dev/null
+++ b/Sources/RadiantProgress/PreviewOperation.swift
@@ -0,0 +1,52 @@
+//
+// PreviewOperation.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+public import Foundation
+
+@MainActor public struct PreviewOperation: ProgressOperation {
+ public let currentValue: ValueType
+
+ public let totalValue: ValueType?
+
+ public let id: URL
+
+ public init(currentValue: ValueType, totalValue: ValueType?, id: URL) {
+ self.currentValue = currentValue
+ self.totalValue = totalValue
+ self.id = id
+ }
+
+ public func execute() async throws {}
+
+ public func cancel() {}
+}
+
+#if canImport(FoundationNetworking)
+ extension URL: @unchecked Sendable {}
+#endif
diff --git a/Sources/RadiantProgress/ProgressOperation.swift b/Sources/RadiantProgress/ProgressOperation.swift
new file mode 100644
index 0000000..43608ee
--- /dev/null
+++ b/Sources/RadiantProgress/ProgressOperation.swift
@@ -0,0 +1,55 @@
+//
+// ProgressOperation.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+public import Foundation
+
+@MainActor public protocol ProgressOperation: Identifiable, Sendable where ID == URL {
+ associatedtype ValueType: BinaryInteger & Sendable
+ var currentValue: ValueType { get }
+ var totalValue: ValueType? { get }
+ func execute() async throws
+}
+
+extension ProgressOperation {
+ public func percentValue(withFractionDigits fractionDigits: Int = 0) -> String? {
+ guard let totalValue else {
+ #warning("logging-note: should we log something here?")
+ return nil
+ }
+ let formatter = NumberFormatter()
+ formatter.maximumFractionDigits = fractionDigits
+ formatter.minimumFractionDigits = fractionDigits
+ let ratioValue = Double(currentValue) / Double(totalValue) * 100.0
+ let string = formatter.string(from: .init(value: ratioValue))
+
+ #warning("logging-note: let's log the calculated percent value if not done somewhere")
+ assert(string != nil)
+ return string
+ }
+}
diff --git a/Sources/RadiantProgress/ProgressOperationProperties.swift b/Sources/RadiantProgress/ProgressOperationProperties.swift
new file mode 100644
index 0000000..957ee90
--- /dev/null
+++ b/Sources/RadiantProgress/ProgressOperationProperties.swift
@@ -0,0 +1,66 @@
+//
+// ProgressOperationProperties.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Observation) && (os(macOS) || os(iOS))
+ public import Foundation
+
+ public struct ProgressOperationProperties: Identifiable, Sendable {
+ internal let imageName: String
+ internal let text: any (StringProtocol & Sendable)
+ internal let progress: FileOperationProgress
+
+ public var id: URL { progress.id }
+
+ public init(
+ imageName: String,
+ text: any (StringProtocol & Sendable),
+ progress: FileOperationProgress
+ ) {
+ self.imageName = imageName
+ self.text = text
+ self.progress = progress
+ }
+ }
+
+ #if canImport(SwiftUI)
+ extension ProgressOperationView {
+ public typealias Properties = ProgressOperationProperties
+ public init(
+ _ properties: Properties,
+ text: @escaping (FileOperationProgress) -> ProgressText,
+ image: @escaping (String) -> Icon
+ ) {
+ self.init(progress: properties.progress, title: properties.text, text: text) {
+ image(properties.imageName)
+ }
+ }
+ }
+ #endif
+
+#endif
diff --git a/Sources/RadiantProgress/ProgressOperationView.swift b/Sources/RadiantProgress/ProgressOperationView.swift
new file mode 100644
index 0000000..0cbec60
--- /dev/null
+++ b/Sources/RadiantProgress/ProgressOperationView.swift
@@ -0,0 +1,75 @@
+//
+// ProgressOperationView.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(SwiftUI)
+ public import SwiftUI
+
+ public struct ProgressOperationView: View {
+ private let progress: FileOperationProgress
+ private let title: any StringProtocol
+ private let text: (FileOperationProgress) -> ProgressText
+ private let icon: () -> Icon
+
+ public var body: some View {
+ VStack {
+ HStack {
+ icon() // .accessibilityIdentifier(Progress.icon.identifier)
+ VStack(alignment: .leading) {
+ Text(title).lineLimit(1).font(.title)
+ // .accessibilityIdentifier(Progress.title.identifier)
+ .accessibilityLabel(title)
+ HStack {
+ if let totalValue = progress.totalValue {
+ ProgressView(value: progress.currentValue, total: totalValue)
+ }
+ else {
+ ProgressView(value: progress.currentValue)
+ }
+ }
+ // .accessibilityIdentifier(Progress.view.identifier)
+ text(progress)
+ }
+ }
+ }
+ .padding()
+ }
+
+ public init(
+ progress: FileOperationProgress,
+ title: any StringProtocol,
+ text: @escaping (FileOperationProgress) -> ProgressText,
+ icon: @escaping () -> Icon
+ ) {
+ self.progress = progress
+ self.title = title
+ self.text = text
+ self.icon = icon
+ }
+ }
+#endif
diff --git a/Sources/RadiantProgress/SetupPublishers.swift b/Sources/RadiantProgress/SetupPublishers.swift
new file mode 100644
index 0000000..4acf6d5
--- /dev/null
+++ b/Sources/RadiantProgress/SetupPublishers.swift
@@ -0,0 +1,96 @@
+//
+// SetupPublishers.swift
+// RadiantKit
+//
+// Created by Leo Dion.
+// Copyright © 2024 BrightDigit.
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the “Software”), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+//
+
+#if canImport(Combine)
+ public import Combine
+
+ public import Foundation
+
+ #warning(
+ "logging-note: can we have some operators for logging the recieved stuff in these subscriptions"
+ )
+ internal struct SetupPublishers {
+ public init() {}
+
+ @MainActor private func setupDownloadPublsihers(_ downloader: ObservableDownloader)
+ -> [AnyCancellable]
+ {
+ var cancellables = [AnyCancellable]()
+
+ downloader.requestSubject.share()
+ .map { downloadRequest -> URLSessionDownloadTask in
+ let task = downloader.session.downloadTask(with: downloadRequest.downloadSourceURL)
+ task.resume()
+ return task
+ }
+ .assign(to: \.task, on: downloader).store(in: &cancellables)
+
+ downloader.resumeDataSubject
+ .map { resumeData in
+ let task = downloader.session.downloadTask(withResumeData: resumeData)
+ task.resume()
+ return task
+ }
+ .assign(to: \.task, on: downloader).store(in: &cancellables)
+
+ return cancellables
+ }
+
+ @MainActor private func setupByteUpdatPublishers(_ downloader: ObservableDownloader)
+ -> [AnyCancellable]
+ {
+ var cancellables = [AnyCancellable]()
+ let downloadUpdate = downloader.downloadUpdate.share()
+ downloadUpdate.map(\.totalBytesWritten).assign(to: \.totalBytesWritten, on: downloader)
+ .store(in: &cancellables)
+ downloadUpdate.map(\.totalBytesExpectedToWrite)
+ .assign(to: \.totalBytesExpectedToWrite, on: downloader).store(in: &cancellables)
+ return cancellables
+ }
+
+ @MainActor internal func callAsFunction(downloader: ObservableDownloader) -> [AnyCancellable] {
+ var cancellables = [AnyCancellable]()
+
+ let destinationFileURLPublisher = downloader.requestSubject.share().map(\.destinationFileURL)
+
+ cancellables.append(contentsOf: setupDownloadPublsihers(downloader))
+
+ Publishers.CombineLatest(downloader.locationURLSubject, destinationFileURLPublisher)
+ .map { sourceURL, destinationURL in
+ Result { try FileManager.default.moveItem(at: sourceURL, to: destinationURL) }
+ }
+ .receive(on: DispatchQueue.main).sink(receiveValue: downloader.onCompletion)
+ .store(in: &cancellables)
+
+ cancellables.append(contentsOf: setupByteUpdatPublishers(downloader))
+
+ return cancellables
+ }
+ }
+#endif
diff --git a/Tests/RadiantKitTests/RadiantKitTests.swift b/Tests/RadiantKitTests/RadiantKitTests.swift
new file mode 100644
index 0000000..e1f4214
--- /dev/null
+++ b/Tests/RadiantKitTests/RadiantKitTests.swift
@@ -0,0 +1,16 @@
+//
+// Test.swift
+// RadiantKit
+//
+// Created by Leo Dion on 9/9/24.
+//
+
+import Testing
+
+struct RadiantKitTests {
+
+ @Test func example() async throws {
+ // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+ }
+
+}
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..951b97b
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,2 @@
+ignore:
+ - "Tests"
diff --git a/macros.json b/macros.json
new file mode 100644
index 0000000..06a5065
--- /dev/null
+++ b/macros.json
@@ -0,0 +1,7 @@
+[
+ {
+ "fingerprint" : "c55848b2aa4b29a4df542b235dfdd792a6fbe341",
+ "packageIdentity" : "swift-testing",
+ "targetName" : "TestingMacros"
+ }
+]
\ No newline at end of file
diff --git a/project.yml b/project.yml
new file mode 100644
index 0000000..ac1e75f
--- /dev/null
+++ b/project.yml
@@ -0,0 +1,13 @@
+name: RadiantKit
+settings:
+ LINT_MODE: ${LINT_MODE}
+packages:
+ RadiantKit:
+ path: .
+aggregateTargets:
+ Lint:
+ buildScripts:
+ - path: Scripts/lint.sh
+ name: Lint
+ basedOnDependencyAnalysis: false
+ schemes: {}