├── .github ├── FUNDING.yml └── workflows │ ├── build-and-test.yml │ └── documentation.yml ├── .gitignore ├── .spi.yml ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── Boutique.xcscheme ├── Demo ├── .gitignore ├── Demo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── Demo │ ├── App │ │ ├── App.BoutiqueDemo.swift │ │ ├── App.State.swift │ │ ├── App.Store.swift │ │ └── ContentView.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── ios-marketing-icon-1024x1024@1x.png │ │ │ ├── ipad-icon-20x20@1x.png │ │ │ ├── ipad-icon-20x20@2x.png │ │ │ ├── ipad-icon-29x29@1x.png │ │ │ ├── ipad-icon-29x29@2x.png │ │ │ ├── ipad-icon-40x40@1x.png │ │ │ ├── ipad-icon-40x40@2x.png │ │ │ ├── ipad-icon-76x76@1x.png │ │ │ ├── ipad-icon-76x76@2x.png │ │ │ ├── ipad-icon-83.5x83.5@2x.png │ │ │ ├── iphone-icon-20x20@2x.png │ │ │ ├── iphone-icon-20x20@3x.png │ │ │ ├── iphone-icon-29x29@1x.png │ │ │ ├── iphone-icon-29x29@2x.png │ │ │ ├── iphone-icon-29x29@3x.png │ │ │ ├── iphone-icon-40x40@2x.png │ │ │ ├── iphone-icon-40x40@3x.png │ │ │ ├── iphone-icon-57x57@1x.png │ │ │ ├── iphone-icon-57x57@2x.png │ │ │ ├── iphone-icon-60x60@2x.png │ │ │ └── iphone-icon-60x60@3x.png │ │ └── Contents.json │ ├── Components │ │ ├── AnimatableGradientView.swift │ │ ├── CarouselView.swift │ │ ├── FavoritesCarouselView.swift │ │ └── RedPandaCardView.swift │ ├── Design │ │ ├── Color.Palette.swift │ │ └── View+Style.swift │ ├── Images │ │ ├── ImagesController.swift │ │ ├── RemoteImage.swift │ │ └── RemoteImageView.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── SwiftUI │ │ └── ScrollFocusController.swift ├── Images │ └── Demo-App.png ├── LICENSE └── README.md ├── Images └── logo.jpg ├── LICENSE ├── Package.resolved ├── Package.swift ├── Performance Profiler ├── Images │ ├── App Icon Original.pxd │ ├── App Icon.png │ ├── Postprocessed App Icon.pxd │ ├── app-demo.png │ └── logo.png ├── Performance Profiler.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── README.md ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── ios-marketing-icon-1024x1024@1x.png │ │ │ ├── ipad-icon-20x20@1x.png │ │ │ ├── ipad-icon-20x20@2x.png │ │ │ ├── ipad-icon-29x29@1x.png │ │ │ ├── ipad-icon-29x29@2x.png │ │ │ ├── ipad-icon-40x40@1x.png │ │ │ ├── ipad-icon-40x40@2x.png │ │ │ ├── ipad-icon-76x76@1x.png │ │ │ ├── ipad-icon-76x76@2x.png │ │ │ ├── ipad-icon-83.5x83.5@2x.png │ │ │ ├── iphone-icon-20x20@2x.png │ │ │ ├── iphone-icon-20x20@3x.png │ │ │ ├── iphone-icon-29x29@1x.png │ │ │ ├── iphone-icon-29x29@2x.png │ │ │ ├── iphone-icon-29x29@3x.png │ │ │ ├── iphone-icon-40x40@2x.png │ │ │ ├── iphone-icon-40x40@3x.png │ │ │ ├── iphone-icon-57x57@1x.png │ │ │ ├── iphone-icon-57x57@2x.png │ │ │ ├── iphone-icon-60x60@2x.png │ │ │ ├── iphone-icon-60x60@3x.png │ │ │ ├── mac-icon-128x128@1x.png │ │ │ ├── mac-icon-128x128@2x.png │ │ │ ├── mac-icon-16x16@1x.png │ │ │ ├── mac-icon-16x16@2x.png │ │ │ ├── mac-icon-256x256@1x.png │ │ │ ├── mac-icon-256x256@2x.png │ │ │ ├── mac-icon-32x32@1x.png │ │ │ ├── mac-icon-32x32@2x.png │ │ │ ├── mac-icon-512x512@1x.png │ │ │ └── mac-icon-512x512@2x.png │ │ └── Contents.json │ ├── Fonts │ │ └── Telegrama-Raw.otf │ ├── Info.plist │ └── Performance Profiler.entitlements └── Sources │ ├── App │ ├── App.Profiler.swift │ └── App.Store.swift │ ├── ContentView │ ├── CompactContentView.swift │ ├── CountButton.swift │ ├── CountButtonContainerView.swift │ ├── OperationProgressView.swift │ ├── PerformanceStatisticView.swift │ ├── RegularContentView.swift │ └── TerminalView.swift │ ├── Design │ ├── Color.Palette.swift │ ├── Font+Custom.swift │ └── View+Shadows.swift │ ├── EstimatedSize │ └── EstimatedSize.swift │ ├── RichNotes │ ├── EstimatedSize+RichNote.swift │ ├── MemoryFormatter.swift │ ├── Models.Demo.swift │ ├── PerformanceStatisticItem.swift │ ├── RichNotesController.swift │ └── RichNotesOperationsView.swift │ └── SwiftUI │ ├── SizeClassDependentValue.swift │ └── SizingResistantView.swift ├── README.md ├── Sources └── Boutique │ ├── Documentation.docc │ └── Articles │ │ ├── Boutique.md │ │ ├── The @Stored Family Of Property Wrappers.md │ │ └── Using Stores.md │ ├── Internal │ ├── AsyncValueSubject.swift │ ├── BoxedValue.swift │ ├── CachedValue.swift │ ├── JSONCoders.swift │ ├── Keychain.swift │ └── Store.ItemRemovalStrategy.swift │ ├── SecurelyStoredValue.swift │ ├── StorableItem.swift │ ├── Store+Identifiable.swift │ ├── Store+Observation.swift │ ├── Store.Operation.swift │ ├── Store.swift │ ├── StoreEvent.swift │ ├── Stored.swift │ ├── StoredValue+Array.swift │ ├── StoredValue+Binding.swift │ ├── StoredValue+Bool.swift │ ├── StoredValue+Dictionary.swift │ ├── StoredValue+KeypathSetter.swift │ └── StoredValue.swift ├── Tests └── BoutiqueTests │ ├── AsyncStoreTests.swift │ ├── BoutiqueItem.swift │ ├── SecurelyStoredValueTests.swift │ ├── StoreEvent.Tests.Requirements.swift │ ├── StoreTests.swift │ ├── StoredTests.swift │ └── StoredValueTests.swift ├── docs ├── css │ ├── 523.e9a069b0.css │ ├── 675.40c3bcb2.css │ ├── documentation-topic.b186e79f.css │ ├── index.ff036a9e.css │ ├── topic.672a9049.css │ └── tutorials-overview.6eb589ed.css ├── data │ └── documentation │ │ ├── boutique.json │ │ └── boutique │ │ ├── keychainerror.json │ │ ├── keychainerror │ │ ├── couldnotaccesskeychain.json │ │ ├── error-implementations.json │ │ ├── errorwithstatus(status:).json │ │ ├── itemnotfound.json │ │ ├── localizeddescription.json │ │ └── missingentitlement.json │ │ ├── keychaingroup.json │ │ ├── keychaingroup │ │ ├── expressiblebyextendedgraphemeclusterliteral-implementations.json │ │ ├── expressiblebyunicodescalarliteral-implementations.json │ │ ├── init(extendedgraphemeclusterliteral:).json │ │ ├── init(stringliteral:).json │ │ ├── init(unicodescalarliteral:).json │ │ ├── init(value:).json │ │ └── value.json │ │ ├── keychainservice.json │ │ ├── keychainservice │ │ ├── expressiblebyextendedgraphemeclusterliteral-implementations.json │ │ ├── expressiblebyunicodescalarliteral-implementations.json │ │ ├── init(extendedgraphemeclusterliteral:).json │ │ ├── init(stringliteral:).json │ │ ├── init(unicodescalarliteral:).json │ │ ├── init(value:).json │ │ └── value.json │ │ ├── securelystoredvalue.json │ │ ├── securelystoredvalue │ │ ├── append(_:).json │ │ ├── binding.json │ │ ├── init(key:service:group:).json │ │ ├── projectedvalue.json │ │ ├── remove().json │ │ ├── replace(_:with:).json │ │ ├── set(_:).json │ │ ├── set(_:to:).json │ │ ├── toggle().json │ │ ├── update(key:value:).json │ │ ├── values.json │ │ └── wrappedvalue.json │ │ ├── storableitem.json │ │ ├── store.json │ │ ├── store │ │ ├── events.json │ │ ├── init(storage:)-1dbuk.json │ │ ├── init(storage:)-2icz.json │ │ ├── init(storage:)-2zxc4.json │ │ ├── init(storage:)-8ky4y.json │ │ ├── init(storage:cacheidentifier:)-11vez.json │ │ ├── init(storage:cacheidentifier:)-1933a.json │ │ ├── insert(_:)-2vg6j.json │ │ ├── insert(_:)-3j9hw.json │ │ ├── insert(_:)-7z2oe.json │ │ ├── insert(_:)-9n4e3.json │ │ ├── items.json │ │ ├── itemshaveloaded().json │ │ ├── operation.json │ │ ├── operation │ │ │ ├── insert(_:)-1nu61.json │ │ │ ├── insert(_:)-32lwk.json │ │ │ ├── remove(_:)-2tqlz.json │ │ │ ├── remove(_:)-8ufsb.json │ │ │ ├── removeall().json │ │ │ └── run().json │ │ ├── previewstore(items:)-1azzy.json │ │ ├── previewstore(items:)-1zymp.json │ │ ├── previewstore(items:cacheidentifier:).json │ │ ├── remove(_:)-1w3lx.json │ │ ├── remove(_:)-3nzlq.json │ │ ├── remove(_:)-51ya6.json │ │ ├── remove(_:)-5dwyv.json │ │ ├── removeall()-1xc24.json │ │ └── removeall()-9zfmy.json │ │ ├── stored.json │ │ ├── stored │ │ ├── init(in:).json │ │ ├── projectedvalue.json │ │ └── wrappedvalue.json │ │ ├── storedvalue.json │ │ ├── storedvalue │ │ ├── append(_:).json │ │ ├── binding.json │ │ ├── init(key:default:storage:).json │ │ ├── init(wrappedvalue:key:storage:).json │ │ ├── projectedvalue.json │ │ ├── replace(_:with:).json │ │ ├── reset().json │ │ ├── set(_:).json │ │ ├── set(_:to:).json │ │ ├── toggle().json │ │ ├── togglepresence(_:).json │ │ ├── update(key:value:).json │ │ ├── values.json │ │ └── wrappedvalue.json │ │ ├── storeevent.json │ │ ├── storeevent │ │ ├── init(from:).json │ │ ├── items.json │ │ ├── operation-swift.enum.json │ │ ├── operation-swift.enum │ │ │ ├── !=(_:_:).json │ │ │ ├── equatable-implementations.json │ │ │ ├── init(from:).json │ │ │ ├── initialized.json │ │ │ ├── insert.json │ │ │ ├── loaded.json │ │ │ └── remove.json │ │ └── operation-swift.property.json │ │ ├── the-@stored-family-of-property-wrappers.json │ │ └── using-stores.json ├── developer-og-twitter.jpg ├── developer-og.jpg ├── documentation │ └── boutique │ │ ├── index.html │ │ ├── keychainerror │ │ ├── couldnotaccesskeychain │ │ │ └── index.html │ │ ├── error-implementations │ │ │ └── index.html │ │ ├── errorwithstatus(status:) │ │ │ └── index.html │ │ ├── index.html │ │ ├── itemnotfound │ │ │ └── index.html │ │ ├── localizeddescription │ │ │ └── index.html │ │ └── missingentitlement │ │ │ └── index.html │ │ ├── keychaingroup │ │ ├── expressiblebyextendedgraphemeclusterliteral-implementations │ │ │ └── index.html │ │ ├── expressiblebyunicodescalarliteral-implementations │ │ │ └── index.html │ │ ├── index.html │ │ ├── init(extendedgraphemeclusterliteral:) │ │ │ └── index.html │ │ ├── init(stringliteral:) │ │ │ └── index.html │ │ ├── init(unicodescalarliteral:) │ │ │ └── index.html │ │ ├── init(value:) │ │ │ └── index.html │ │ └── value │ │ │ └── index.html │ │ ├── keychainservice │ │ ├── expressiblebyextendedgraphemeclusterliteral-implementations │ │ │ └── index.html │ │ ├── expressiblebyunicodescalarliteral-implementations │ │ │ └── index.html │ │ ├── index.html │ │ ├── init(extendedgraphemeclusterliteral:) │ │ │ └── index.html │ │ ├── init(stringliteral:) │ │ │ └── index.html │ │ ├── init(unicodescalarliteral:) │ │ │ └── index.html │ │ ├── init(value:) │ │ │ └── index.html │ │ └── value │ │ │ └── index.html │ │ ├── securelystoredvalue │ │ ├── append(_:) │ │ │ └── index.html │ │ ├── binding │ │ │ └── index.html │ │ ├── index.html │ │ ├── init(key:service:group:) │ │ │ └── index.html │ │ ├── projectedvalue │ │ │ └── index.html │ │ ├── remove() │ │ │ └── index.html │ │ ├── replace(_:with:) │ │ │ └── index.html │ │ ├── set(_:) │ │ │ └── index.html │ │ ├── set(_:to:) │ │ │ └── index.html │ │ ├── toggle() │ │ │ └── index.html │ │ ├── update(key:value:) │ │ │ └── index.html │ │ ├── values │ │ │ └── index.html │ │ └── wrappedvalue │ │ │ └── index.html │ │ ├── storableitem │ │ └── index.html │ │ ├── store │ │ ├── events │ │ │ └── index.html │ │ ├── index.html │ │ ├── init(storage:)-1dbuk │ │ │ └── index.html │ │ ├── init(storage:)-2icz │ │ │ └── index.html │ │ ├── init(storage:)-2zxc4 │ │ │ └── index.html │ │ ├── init(storage:)-8ky4y │ │ │ └── index.html │ │ ├── init(storage:cacheidentifier:)-11vez │ │ │ └── index.html │ │ ├── init(storage:cacheidentifier:)-1933a │ │ │ └── index.html │ │ ├── insert(_:)-2vg6j │ │ │ └── index.html │ │ ├── insert(_:)-3j9hw │ │ │ └── index.html │ │ ├── insert(_:)-7z2oe │ │ │ └── index.html │ │ ├── insert(_:)-9n4e3 │ │ │ └── index.html │ │ ├── items │ │ │ └── index.html │ │ ├── itemshaveloaded() │ │ │ └── index.html │ │ ├── operation │ │ │ ├── index.html │ │ │ ├── insert(_:)-1nu61 │ │ │ │ └── index.html │ │ │ ├── insert(_:)-32lwk │ │ │ │ └── index.html │ │ │ ├── remove(_:)-2tqlz │ │ │ │ └── index.html │ │ │ ├── remove(_:)-8ufsb │ │ │ │ └── index.html │ │ │ ├── removeall() │ │ │ │ └── index.html │ │ │ └── run() │ │ │ │ └── index.html │ │ ├── previewstore(items:)-1azzy │ │ │ └── index.html │ │ ├── previewstore(items:)-1zymp │ │ │ └── index.html │ │ ├── previewstore(items:cacheidentifier:) │ │ │ └── index.html │ │ ├── remove(_:)-1w3lx │ │ │ └── index.html │ │ ├── remove(_:)-3nzlq │ │ │ └── index.html │ │ ├── remove(_:)-51ya6 │ │ │ └── index.html │ │ ├── remove(_:)-5dwyv │ │ │ └── index.html │ │ ├── removeall()-1xc24 │ │ │ └── index.html │ │ └── removeall()-9zfmy │ │ │ └── index.html │ │ ├── stored │ │ ├── index.html │ │ ├── init(in:) │ │ │ └── index.html │ │ ├── projectedvalue │ │ │ └── index.html │ │ └── wrappedvalue │ │ │ └── index.html │ │ ├── storedvalue │ │ ├── append(_:) │ │ │ └── index.html │ │ ├── binding │ │ │ └── index.html │ │ ├── index.html │ │ ├── init(key:default:storage:) │ │ │ └── index.html │ │ ├── init(wrappedvalue:key:storage:) │ │ │ └── index.html │ │ ├── projectedvalue │ │ │ └── index.html │ │ ├── replace(_:with:) │ │ │ └── index.html │ │ ├── reset() │ │ │ └── index.html │ │ ├── set(_:) │ │ │ └── index.html │ │ ├── set(_:to:) │ │ │ └── index.html │ │ ├── toggle() │ │ │ └── index.html │ │ ├── togglepresence(_:) │ │ │ └── index.html │ │ ├── update(key:value:) │ │ │ └── index.html │ │ ├── values │ │ │ └── index.html │ │ └── wrappedvalue │ │ │ └── index.html │ │ ├── storeevent │ │ ├── index.html │ │ ├── init(from:) │ │ │ └── index.html │ │ ├── items │ │ │ └── index.html │ │ ├── operation-swift.enum │ │ │ ├── !=(_:_:) │ │ │ │ └── index.html │ │ │ ├── equatable-implementations │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ ├── init(from:) │ │ │ │ └── index.html │ │ │ ├── initialized │ │ │ │ └── index.html │ │ │ ├── insert │ │ │ │ └── index.html │ │ │ ├── loaded │ │ │ │ └── index.html │ │ │ └── remove │ │ │ │ └── index.html │ │ └── operation-swift.property │ │ │ └── index.html │ │ ├── the-@stored-family-of-property-wrappers │ │ └── index.html │ │ └── using-stores │ │ └── index.html ├── favicon.ico ├── favicon.svg ├── img │ ├── added-icon.832a5d2c.svg │ ├── deprecated-icon.7bf1740a.svg │ └── modified-icon.efb2697d.svg ├── index.html ├── index │ └── index.json ├── js │ ├── 337.274a8ccc.js │ ├── 37.3cabdf6d.js │ ├── 523.3af1b2ef.js │ ├── 903.5a8b9c15.js │ ├── chunk-vendors.bdb7cbba.js │ ├── documentation-topic.f9ef3692.js │ ├── highlight-js-bash-js.702f0c5c.js │ ├── highlight-js-c-js.063069d3.js │ ├── highlight-js-cpp-js.458a9ae4.js │ ├── highlight-js-css-js.bfc4251f.js │ ├── highlight-js-custom-markdown.78c9f6ed.js │ ├── highlight-js-custom-swift.738731d1.js │ ├── highlight-js-diff-js.4db9a783.js │ ├── highlight-js-http-js.f78e83c2.js │ ├── highlight-js-java-js.4fe21e94.js │ ├── highlight-js-javascript-js.dfc9d16d.js │ ├── highlight-js-json-js.2a1856ba.js │ ├── highlight-js-llvm-js.26121771.js │ ├── highlight-js-markdown-js.a2f456af.js │ ├── highlight-js-objectivec-js.74dea052.js │ ├── highlight-js-perl-js.da6eda82.js │ ├── highlight-js-php-js.c458ffa4.js │ ├── highlight-js-python-js.60354774.js │ ├── highlight-js-ruby-js.7272231f.js │ ├── highlight-js-scss-js.adcd11a2.js │ ├── highlight-js-shell-js.0ad5b20f.js │ ├── highlight-js-swift-js.bdd5bff5.js │ ├── highlight-js-xml-js.0d78f903.js │ ├── index.91ed7402.js │ ├── topic.2687cdff.js │ └── tutorials-overview.2eff1231.js ├── metadata.json └── theme-settings.json └── llms.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mergesort] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build-and-test: 9 | runs-on: macos-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Build 14 | run: swift build -v 15 | - name: Run tests 16 | run: swift test -v 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update_documentation: 11 | name: Update documentation 12 | runs-on: macos-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | - name: Setup Swift version 17 | uses: swift-actions/setup-swift@v2 18 | with: 19 | swift-version: "5.10.1" 20 | - name: Generate documentation 21 | uses: fwcd/swift-docc-action@v1 22 | with: 23 | target: Boutique 24 | output: ./docs 25 | hosting-base-path: Boutique 26 | disable-indexing: 'true' 27 | transform-for-static-hosting: 'true' 28 | - name: Commit documentation 29 | run: | 30 | git config user.name github-actions 31 | git config user.email github-actions@github.com 32 | git add ./docs/** 33 | git commit -m "Generating documentation" 34 | git push 35 | 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: ios 5 | documentation_targets: [Boutique] -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/.gitignore: -------------------------------------------------------------------------------- 1 | MVC-Demo.xcodeproj/project.xcworkspace/xcuserdata/ 2 | MVC-Demo.xcodeproj/xcuserdata/ 3 | UMVC-Demo.xcodeproj/project.xcworkspace/xcuserdata/* 4 | UMVC-Demo.xcodeproj/xcuserdata/mergesort.xcuserdatad/* 5 | MVS-Demo.xcodeproj/project.xcworkspace/xcuserdata/* 6 | MVS-Demo.xcodeproj/xcuserdata/mergesort.xcuserdatad/* 7 | MVCS-Demo.xcodeproj/project.xcworkspace/xcuserdata/mergesort.xcuserdatad/* 8 | MVCS-Demo.xcodeproj/project.xcworkspace/xcuserdata/mergesort.xcuserdatad/* 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "70ca90eb3fbfec4a3d93a1da9661356805637a48d9b94fd45ff8771cc4dd7582", 3 | "pins" : [ 4 | { 5 | "identity" : "bodega", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/mergesort/Bodega.git", 8 | "state" : { 9 | "revision" : "bfd8871e9c2590d31b200e54c75428a71483afdf", 10 | "version" : "2.1.3" 11 | } 12 | }, 13 | { 14 | "identity" : "sqlite.swift", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/stephencelis/SQLite.swift.git", 17 | "state" : { 18 | "revision" : "4d543d811ee644fa4cc4bfa0be996b4dd6ba0f54", 19 | "version" : "0.13.3" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-collections", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-collections", 26 | "state" : { 27 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 28 | "version" : "1.0.4" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-docc-plugin", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-docc-plugin", 35 | "state" : { 36 | "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6", 37 | "version" : "1.0.0" 38 | } 39 | } 40 | ], 41 | "version" : 3 42 | } 43 | -------------------------------------------------------------------------------- /Demo/Demo/App/App.BoutiqueDemo.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct BoutiqueDemoApp: App { 5 | @State private var appState = AppState() 6 | 7 | var body: some Scene { 8 | WindowGroup { 9 | ContentView() 10 | .environment(appState) 11 | .onAppear(perform: { 12 | // Saving the last time the app was opened to demonstrate how @StoredValue 13 | // persists values. The next time you open the app it should print 14 | // the timestamp the app was last lauched, no databases needed. 15 | print("App last opened:", appState.lastAppLaunchTimestamp ?? "Never") 16 | 17 | let currentTime = Date.now 18 | appState.$lastAppLaunchTimestamp.set(currentTime) 19 | print("Current time is \(currentTime). You will see that timestamp next app launch.") 20 | }) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Demo/Demo/App/App.State.swift: -------------------------------------------------------------------------------- 1 | import Boutique 2 | import Foundation 3 | 4 | @Observable 5 | final class AppState { 6 | @ObservationIgnored 7 | @StoredValue(key: "funkyRedPandaModeEnabled") 8 | var funkyRedPandaModeEnabled = false 9 | 10 | @ObservationIgnored 11 | @StoredValue(key: "fetchedRedPandas") 12 | var fetchedRedPandas: [URL] = [] 13 | 14 | @ObservationIgnored 15 | @StoredValue(key: "lastAppLaunchTimestamp") 16 | var lastAppLaunchTimestamp = nil 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Demo/App/App.Store.swift: -------------------------------------------------------------------------------- 1 | import Boutique 2 | 3 | extension Store where Item == RemoteImage { 4 | /// The app's default images store`. 5 | /// 6 | /// Stores are low-cost and can be plugged into Controllers interchangeably, or even accessed independently. 7 | /// What does this mean? We decouple Controllers from Stores so if you want one global store for images 8 | /// you want cached and accessible throughout the app, you can have that. Or if you want to create many 9 | /// small or even temp stores, that's perfectly fine too, in fact that makes it great for testing. 10 | /// 11 | /// Stores are initialized with a `StorageEngine`, the data source that will be persisting your data. 12 | /// We're using one of the two `StorageEngine`s provided by Bodega, the `SQLiteStorageEngine`. 13 | /// The `SQLiteStorageEngine` is a safe, fast, and easy database to based on SQLite, a great default! 14 | /// 15 | /// Another built-in `StorageEngine` you can use is the `DiskStorageEngine`, a `StorageEngine` 16 | /// based on storing items as files on the file system. The ``DiskStorageEngine`` prioritizes 17 | /// simplicity over speed, it is very easy to use and understand. 18 | /// 19 | /// What's super cool about the `StorageEngine` protocol is that you can conform to it 20 | /// to integrate your own persistence layer into Boutique. If you're using Realm, Core Data, CloudKit, 21 | /// or even your own API server, you can model them as a `StorageEngine` to use in a `Store`. 22 | /// 23 | /// This will enable you to have a realtime updating app just like any other Boutique-based app, 24 | /// but with your very own data layer. You can even endlessly compose `StorageEngine`s to create a 25 | /// complex data pipeline that hits your API and saves items into a database, all in one API call. 26 | static let imagesStore = Store( 27 | storage: SQLiteStorageEngine.default(appendingPath: "Images") 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /Demo/Demo/App/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | @Environment(AppState.self) private var appState 5 | 6 | @StateObject private var carouselFocusController = ScrollFocusController() 7 | @State private var imagesController = ImagesController() 8 | 9 | var body: some View { 10 | VStack(spacing: 0.0) { 11 | FavoritesCarouselView() 12 | .padding(.bottom, 8.0) 13 | .environmentObject(carouselFocusController) 14 | .environment(imagesController) 15 | 16 | Divider() 17 | 18 | Spacer() 19 | 20 | RedPandaCardView() 21 | .environmentObject(carouselFocusController) 22 | } 23 | .padding(.horizontal, 16.0) 24 | .background(Color.palette.background) 25 | .onChange(of: self.appState.funkyRedPandaModeEnabled) { oldValue, newValue in 26 | print("Funky red panda mode was \(oldValue) and now is \(newValue)") 27 | } 28 | .task({ 29 | await self.monitorImageStoreEvents() 30 | }) 31 | } 32 | } 33 | 34 | private extension ContentView { 35 | func monitorImageStoreEvents() async { 36 | for await event in self.imagesController.$images.events { 37 | switch event.operation { 38 | 39 | case .initialized: 40 | print("[Store Event: initial] Our Images Store has initialized") 41 | 42 | case .loaded: 43 | print("[Store Event: loaded] Our Images Store has loaded with images", event.items.map(\.url)) 44 | 45 | case .insert: 46 | print("[Store Event: insert] Our Images Store inserted images", event.items.map(\.url)) 47 | 48 | case .remove: 49 | print("[Store Event: remove] Our Images Store removed images", event.items.map(\.url)) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ios-marketing-icon-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ios-marketing-icon-1024x1024@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-20x20@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-20x20@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-29x29@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-29x29@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-40x40@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-40x40@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-76x76@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-76x76@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/ipad-icon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-20x20@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-20x20@3x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@3x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-40x40@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-40x40@3x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-57x57@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-57x57@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-60x60@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Demo/Assets.xcassets/AppIcon.appiconset/iphone-icon-60x60@3x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Components/AnimatableGradientView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AnimatableGradientView: View { 4 | let fromGradient: Gradient 5 | let toGradient: Gradient 6 | let duration: TimeInterval = 1.5 7 | 8 | @State private var progress = 0.0 9 | 10 | var body: some View { 11 | self.animatableGradient( 12 | fromGradient: fromGradient, 13 | toGradient: toGradient, 14 | progress: progress 15 | ) 16 | .onAppear(perform: { 17 | withAnimation(.linear(duration: duration).repeatForever(autoreverses: true)) { 18 | self.progress = 1.0 19 | } 20 | }) 21 | } 22 | } 23 | 24 | extension View { 25 | func animatableGradient(fromGradient: Gradient, toGradient: Gradient, progress: CGFloat) -> some View { 26 | self.modifier(AnimatableGradientModifier(fromGradient: fromGradient, toGradient: toGradient, progress: progress)) 27 | } 28 | } 29 | 30 | struct AnimatableGradientModifier: AnimatableModifier { 31 | let fromGradient: Gradient 32 | let toGradient: Gradient 33 | var progress: CGFloat = 0.0 34 | 35 | var animatableData: CGFloat { 36 | get { progress } 37 | set { progress = newValue } 38 | } 39 | 40 | func body(content: Content) -> some View { 41 | var gradientColors = [Color]() 42 | 43 | for i in 0.. Color { 54 | guard let fromColor = fromColor.cgColor.components else { return Color(fromColor) } 55 | guard let toColor = toColor.cgColor.components else { return Color(toColor) } 56 | 57 | let red = fromColor[0] + (toColor[0] - fromColor[0]) * progress 58 | let green = fromColor[1] + (toColor[1] - fromColor[1]) * progress 59 | let blue = fromColor[2] + (toColor[2] - fromColor[2]) * progress 60 | 61 | return Color(red: Double(red), green: Double(green), blue: Double(blue)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Demo/Demo/Components/CarouselView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A View for displaying content in a horizontally scrolling grid. 4 | struct CarouselView: View { 5 | var items: [Item] 6 | var contentView: (Item) -> ContentView 7 | 8 | @EnvironmentObject private var focusController: ScrollFocusController 9 | @State private var customPreferenceKey: String = "" 10 | 11 | var body: some View { 12 | ScrollView(.horizontal, showsIndicators: false) { 13 | ScrollViewReader { reader in 14 | HStack(alignment: .top, spacing: 16.0) { 15 | ForEach(items) { item in 16 | contentView(item) 17 | .tag(item.id) 18 | } 19 | } 20 | .onReceive(self.focusController.publisher, perform: { id in 21 | if let id = id { 22 | withAnimation { 23 | reader.scrollTo(id) 24 | } 25 | } 26 | }) 27 | } 28 | } 29 | .listRowSeparator(.hidden) 30 | .listRowBackground(Color.clear) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Demo/Demo/Design/Color.Palette.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Color { 4 | static let palette = Color.Palette() 5 | } 6 | 7 | extension Color { 8 | struct Palette { 9 | var primary: Color { 10 | Color(red: colorValue(195), green: colorValue(82), blue: colorValue(43)) 11 | } 12 | 13 | var secondary: Color { 14 | Color(red: colorValue(227), green: colorValue(114), blue: colorValue(75)) 15 | } 16 | 17 | var tertiary: Color { 18 | Color(red: colorValue(255), green: colorValue(162), blue: colorValue(123)) 19 | } 20 | 21 | var background: Color { 22 | Color(red: colorValue(236), green: colorValue(240), blue: colorValue(241)) 23 | } 24 | 25 | var primaryRainbowGradient: [Color] { 26 | Array(self.rainbowGradientColors.prefix(2)) 27 | } 28 | 29 | var secondaryRainbowGradient: [Color] { 30 | Array(self.rainbowGradientColors.suffix(2)) 31 | } 32 | } 33 | } 34 | 35 | private extension Color.Palette { 36 | var rainbowGradientColors: [Color] { 37 | [Color.purple, Color.yellow, Color.blue] 38 | } 39 | } 40 | 41 | // I'm too lazy to build a real palette for this project so with this mediocre code. 42 | private extension Color { 43 | static func colorValue(_ fromRGBValue: Int) -> Double { 44 | return Double(fromRGBValue)/255.0 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Demo/Demo/Design/View+Style.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | func centerCroppedCardStyle() -> some View { 5 | self.scaledToFill() 6 | .clipped() 7 | .cornerRadius(8.0) 8 | } 9 | 10 | func primaryBorder() -> some View { 11 | self.overlay( 12 | RoundedRectangle(cornerRadius: 8.0) 13 | .stroke(Color.palette.primary, lineWidth: 4.0) 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Demo/Images/RemoteImage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Bodega 3 | 4 | /// A type representing the API response of an image from the API we're interacting with. 5 | struct RemoteImage: Codable, Equatable, Identifiable { 6 | let createdAt: Date 7 | let url: URL 8 | let width: Float 9 | let height: Float 10 | let dataRepresentation: Data 11 | 12 | // We're using a `CacheKey` from Bodega (one of Boutique's dependencies) 13 | // because it's file-system safe, unlike `url.absoluteString`. 14 | // 15 | // In most cases using a plain string will be perfectly sufficient but URLs can be up to 4096 characters 16 | // and files on disk can only be 256 characters, so I recommend using a `CacheKey` when possible. 17 | // But it's worth emphasizing, using a String should be perfectly acceptable with pretty much any non-URL data type. 18 | var id: String { 19 | return CacheKey(url: self.url).value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Demo/Demo/Images/RemoteImageView.swift: -------------------------------------------------------------------------------- 1 | import Boutique 2 | import SwiftUI 3 | 4 | /// A view that displays a `RemoteImage`. 5 | struct RemoteImageView: View { 6 | var image: RemoteImage 7 | 8 | var body: some View { 9 | let currentImage = UIImage(data: image.dataRepresentation) ?? UIImage() 10 | 11 | Image(uiImage: currentImage) 12 | .resizable() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/SwiftUI/ScrollFocusController.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import SwiftUI 3 | 4 | /// A controller that allows a parent to subscribe to a child's tap events for the purpose of scrolling. 5 | final class ScrollFocusController: ObservableObject { 6 | private let currentValueSubject = CurrentValueSubject(nil) 7 | 8 | var publisher: AnyPublisher { 9 | return self.currentValueSubject.eraseToAnyPublisher() 10 | } 11 | 12 | func scrollTo(_ remoteImage: T) { 13 | self.currentValueSubject.value = remoteImage 14 | self.currentValueSubject.value = nil 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/Images/Demo-App.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Demo/Images/Demo-App.png -------------------------------------------------------------------------------- /Demo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Joe Fabisevich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Demo/README.md: -------------------------------------------------------------------------------- 1 | # Boutique: A Simple, Familiar, Yet Powerful Approach To Building SwiftUI, UIKit, and AppKit Apps 2 | 3 |

4 | 7 |

8 | 9 | Welcome to a demo of building an app with Boutique. This repo is primarily oriented to sharing the Boutique demo code. If you'd like to learn more about how Model View Controller Store, Boutique, and Bodega work, please read the walkthrough [in this post](https://build.ms/2022/06/22/model-view-controller-store/) or reference [Boutique's documentation](https://build.ms/boutique/docs). 10 | 11 | The best way to explain Boutique and Model View Controller Store is to show you what it is. The idea is so small that I'm convinced you can look at the code in this repo and know how it works almost immediately, there's actually very little to learn. Boutique is a library I've developed to provide a batteries-included `Store`, and doesn't require you to change your apps to use it. 12 | 13 | Boutique requires no tricks to use, does *no behind the scenes magic*, and doesn't resort to shenanigans like runtime hacking to achieve a great developer experience. Boutique's `Store` is a dual-layered memory and disk cache which ***lets you build apps that update in real time with full offline storage with three lines of code and an incredibly simple API***. That may sound a bit fancy but all it means is that when you save an object into the `Store`, it also saves that object to a database. This persistence is powered under the hood by [Bodega](https://github.com/mergesort/Bodega), an actor-based library I've developed for building data storage engines. 14 | 15 | If you think this sounds too good to be true I recommend you play with the app yourself and see how simple it really is. 16 | 17 |

18 | Plus don't you want to look at some cute red pandas? 19 |

20 | 21 | https://user-images.githubusercontent.com/716513/174133310-239d7da7-8a0d-48e6-a909-c9a121078f74.mov 22 | 23 | > **Note** 24 | > While this demo app stores images in Boutique, storing images or other binary data in Boutique is not recommended. The reason for this is that storing images in Boutique can balloon up your app's memory, so the same way you wouldn't put images into a database you should avoid storing images in Boutique. 25 | > 26 | > This was something I only considered after releasing Boutique, and this demo project is still great for demonstrating what Boutique can do, but if you're storing images wouldn't scale to storing the thousands of objects Boutique can handle otherwise. I'm working on an example wtihout images to make sure it's clearer to not use Boutique as an image cache, but I ask folks be patient as I've been overwhelmed with tons of [really positive] feedback. With that said, [Bodega](https://github.com/mergesort/Bodega) is a great way to store binary data to disk, and I would highly recommend it for downloading and storing images. 27 | -------------------------------------------------------------------------------- /Images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Images/logo.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Joe Fabisevich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "cc0a5555f8ada9d39eb7aa3ebf517983770c3fb59b40991749c3f025fce9ea00", 3 | "pins" : [ 4 | { 5 | "identity" : "bodega", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/mergesort/Bodega.git", 8 | "state" : { 9 | "revision" : "bfd8871e9c2590d31b200e54c75428a71483afdf", 10 | "version" : "2.1.3" 11 | } 12 | }, 13 | { 14 | "identity" : "sqlite.swift", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/stephencelis/SQLite.swift.git", 17 | "state" : { 18 | "revision" : "4d543d811ee644fa4cc4bfa0be996b4dd6ba0f54", 19 | "version" : "0.13.3" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-collections", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-collections", 26 | "state" : { 27 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 28 | "version" : "1.0.4" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-docc-plugin", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-docc-plugin", 35 | "state" : { 36 | "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6", 37 | "version" : "1.0.0" 38 | } 39 | } 40 | ], 41 | "version" : 3 42 | } 43 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Boutique", 8 | platforms: [ 9 | .iOS(.v17), 10 | .macOS(.v14), 11 | ], 12 | products: [ 13 | .library( 14 | name: "Boutique", 15 | targets: ["Boutique"]), 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/mergesort/Bodega.git", exact: Version(2, 1, 3)), 19 | .package(url: "https://github.com/apple/swift-collections", from: Version(1, 0, 3)), 20 | .package(url: "https://github.com/apple/swift-docc-plugin", from: Version(1, 0, 0)), 21 | ], 22 | targets: [ 23 | .target( 24 | name: "Boutique", 25 | dependencies: [ 26 | .byName(name: "Bodega"), 27 | .product(name: "OrderedCollections", package: "swift-collections") 28 | ], 29 | exclude: [ 30 | "../../Images", 31 | "../../Performance Profiler", 32 | ], 33 | swiftSettings: [ 34 | .enableExperimentalFeature("StrictConcurrency"), 35 | .enableUpcomingFeature("DisableOutwardActorInference"), 36 | .enableUpcomingFeature("GlobalActorIsolatedTypesUsability"), 37 | .enableUpcomingFeature("InferSendableFromCaptures"), 38 | .define("ENABLE_TESTABILITY", .when(configuration: .debug)) 39 | ] 40 | ), 41 | .testTarget( 42 | name: "BoutiqueTests", 43 | dependencies: ["Boutique"] 44 | ), 45 | ] 46 | ) 47 | -------------------------------------------------------------------------------- /Performance Profiler/Images/App Icon Original.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Images/App Icon Original.pxd -------------------------------------------------------------------------------- /Performance Profiler/Images/App Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Images/App Icon.png -------------------------------------------------------------------------------- /Performance Profiler/Images/Postprocessed App Icon.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Images/Postprocessed App Icon.pxd -------------------------------------------------------------------------------- /Performance Profiler/Images/app-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Images/app-demo.png -------------------------------------------------------------------------------- /Performance Profiler/Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Images/logo.png -------------------------------------------------------------------------------- /Performance Profiler/Performance Profiler.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Performance Profiler/Performance Profiler.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Performance Profiler/Performance Profiler.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "70ca90eb3fbfec4a3d93a1da9661356805637a48d9b94fd45ff8771cc4dd7582", 3 | "pins" : [ 4 | { 5 | "identity" : "bodega", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/mergesort/Bodega.git", 8 | "state" : { 9 | "revision" : "bfd8871e9c2590d31b200e54c75428a71483afdf", 10 | "version" : "2.1.3" 11 | } 12 | }, 13 | { 14 | "identity" : "sqlite.swift", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/stephencelis/SQLite.swift.git", 17 | "state" : { 18 | "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", 19 | "version" : "0.15.3" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-collections", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-collections", 26 | "state" : { 27 | "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", 28 | "version" : "1.1.1" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-docc-plugin", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-docc-plugin", 35 | "state" : { 36 | "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", 37 | "version" : "1.3.0" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-docc-symbolkit", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-docc-symbolkit", 44 | "state" : { 45 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 46 | "version" : "1.0.0" 47 | } 48 | } 49 | ], 50 | "version" : 3 51 | } 52 | -------------------------------------------------------------------------------- /Performance Profiler/README.md: -------------------------------------------------------------------------------- 1 |

2 | 7 |

8 | 9 | Thank you so much [@Sandor](https://dribbble.com/sandor) for the icon, if you think this looks neat as hell like I do, you should commission some of him for some icon work! And thank you [@joeyabanks](https://twitter.com/joeyabanks) for helping me turn it into an app icon. 10 | 11 | --- 12 | 13 | As I was building Boutique it became apparent to me that I needed a way to see the effects of changes I was making in a more real-world manner than using unit tests to measure performance changes. This performance profiler app helps measure those changes and allows me to see the outcome of any changes I make, and pinpoint performance hotspots. It also allows you the user, anyone who's interested in using Boutique, to see what kind of performance they can expect in their apps. 14 | 15 | It also looks pretty dope, so I hope you find this app useful. The design is a twist on a terminal emulator, because when I think performance needs I think fun classic hardware. 16 | 17 | ![Boutique Performance Profiler App Demo Image](Images/app-demo.png) 18 | 19 | --- 20 | 21 | ### About me 22 | 23 | Hi, I'm [Joe](http://fabisevi.ch) everywhere on the web, but especially on [Twitter](https://twitter.com/mergesort). 24 | 25 | ### License 26 | 27 | See the [license](../LICENSE) for more information about how you can use Boutique. 28 | 29 | ### Sponsorship 30 | 31 | Boutique is a labor of love to help developers build better apps, making it easier for you to unlock your creativity and make something amazing for your yourself and your users. If you find Boutique valuable I would really appreciate it if you'd consider helping [sponsor my open source work](https://github.com/sponsors/mergesort), so I can continue to work on projects like Boutique to help developers like yourself. 32 | -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing-icon-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing-icon-1024x1024@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-20x20@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-20x20@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-29x29@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-29x29@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-40x40@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-40x40@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-76x76@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-76x76@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/ipad-icon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-20x20@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-20x20@3x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-29x29@3x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-40x40@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-40x40@3x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-57x57@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-57x57@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-60x60@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/iphone-icon-60x60@3x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-128x128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-128x128@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-128x128@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-16x16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-16x16@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-16x16@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-256x256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-256x256@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-256x256@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-32x32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-32x32@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-32x32@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-512x512@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-512x512@1x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Assets.xcassets/AppIcon.appiconset/mac-icon-512x512@2x.png -------------------------------------------------------------------------------- /Performance Profiler/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Performance Profiler/Resources/Fonts/Telegrama-Raw.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/Performance Profiler/Resources/Fonts/Telegrama-Raw.otf -------------------------------------------------------------------------------- /Performance Profiler/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIAppFonts 6 | 7 | Telegrama-Raw.otf 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Performance Profiler/Resources/Performance Profiler.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/App/App.Profiler.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct ProfilerApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | 12 | // A wrapper around each ContentView so we can access the Environment which is unavailable at the `App` level 13 | private struct ContentView: View { 14 | @Environment(\.isRegularSizeClass) private var isRegularSizeClass 15 | 16 | var body: some View { 17 | if self.isRegularSizeClass { 18 | // All iPads 19 | RegularContentView() 20 | } else { 21 | // All iPhones including Plus/Max sized devices 22 | CompactContentView() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/App/App.Store.swift: -------------------------------------------------------------------------------- 1 | import Boutique 2 | 3 | extension Store where Item == RichNote { 4 | static let notesStore = Store( 5 | storage: SQLiteStorageEngine.default(appendingPath: "Notes"), 6 | cacheIdentifier: \.id 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/ContentView/CountButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CountButton: View { 4 | let title: String 5 | let color: Color 6 | let action: () -> Void 7 | 8 | var body: some View { 9 | Button(title, action: action) 10 | .font(.title2) 11 | .buttonStyle(.borderedProminent) 12 | .foregroundColor(.white) 13 | .tint(color) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/ContentView/OperationProgressView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct OperationProgressView: View { 4 | var operation: RichNotesOperation 5 | 6 | @SizeClassDependentValue(regular: UIFont.TextStyle.title3, compact: UIFont.TextStyle.body) private var fontStyle 7 | 8 | var body: some View { 9 | Text(self.title) 10 | .textShadow() 11 | .padding(16.0) 12 | .background(Color.palette.terminalBackground) 13 | .cornerRadius(8.0) 14 | .foregroundColor(.white) 15 | .font(.telegramaRaw(style: fontStyle)) 16 | } 17 | } 18 | 19 | private extension OperationProgressView { 20 | var title: String { 21 | switch self.operation.action { 22 | case .add: "Insert" 23 | case .remove: "Remove" 24 | case .loading: "Operation in Progress…" 25 | case .none: "Operation Complete" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/ContentView/PerformanceStatisticView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct PerformanceStatisticView: View { 4 | let leadingText: String 5 | let trailingText: String 6 | 7 | @SizeClassDependentValue(regular: UIFont.TextStyle.title1, compact: UIFont.TextStyle.body) private var fontStyle 8 | 9 | var body: some View { 10 | HStack(alignment: .firstTextBaseline) { 11 | Text(leadingText) 12 | .shadow(color: .palette.terminalYellow.opacity(0.7), radius: 2.0, x: 2.0, y: 2.0) 13 | 14 | Spacer() 15 | 16 | Text(trailingText) 17 | .shadow(color: .palette.terminalYellow.opacity(0.7), radius: 2.0, x: 2.0, y: 2.0) 18 | } 19 | .font(.telegramaRaw(style: fontStyle)) 20 | .padding(.horizontal, 16.0) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/ContentView/TerminalView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct TerminalView: View { 4 | let statistics: [PerformanceStatisticItem] 5 | 6 | var body: some View { 7 | VStack(spacing: 16.0) { 8 | Spacer().frame(height: 16.0) 9 | 10 | ForEach(statistics) { statistic in 11 | PerformanceStatisticView( 12 | leadingText: statistic.title, 13 | trailingText: statistic.measurement 14 | ) 15 | .foregroundColor(Color.palette.terminalYellow) 16 | } 17 | 18 | Spacer().frame(height: 16.0) 19 | } 20 | .background(Color.palette.terminalBackground) 21 | .cornerRadius(16.0) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/Design/Color.Palette.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Color { 4 | static let palette = Color.Palette() 5 | } 6 | 7 | extension Color { 8 | struct Palette { 9 | var terminalYellow: Color { 10 | Color(red: colorValue(242), green: colorValue(168), blue: colorValue(59)) 11 | } 12 | 13 | var terminalOrange: Color { 14 | Color(red: colorValue(230), green: colorValue(94), blue: colorValue(41)) 15 | } 16 | 17 | var terminalBackground: Color { 18 | Color(red: colorValue(20), green: colorValue(20), blue: colorValue(20)) 19 | } 20 | 21 | var appBackground: Color { 22 | Color(red: colorValue(10), green: colorValue(10), blue: colorValue(10)) 23 | } 24 | 25 | var add: Color { 26 | Color(red: colorValue(87), green: colorValue(189), blue: colorValue(83)) 27 | } 28 | 29 | var remove: Color { 30 | Color(red: colorValue(153), green: colorValue(30), blue: colorValue(23)) 31 | } 32 | } 33 | } 34 | 35 | // I'm too lazy to build a real palette for this project so with this mediocre code. 36 | private extension Color { 37 | static func colorValue(_ fromRGBValue: Int) -> Double { 38 | return Double(fromRGBValue)/255.0 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/Design/Font+Custom.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Font { 4 | static func telegramaRaw(style: UIFont.TextStyle, weight: Font.Weight = .regular) -> Font { 5 | Font.custom( 6 | "Telegrama Raw", 7 | size: UIFont.preferredFont(forTextStyle: style).pointSize 8 | ) 9 | .weight(weight) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/Design/View+Shadows.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | func ghostEffectShadow(_ color: Color) -> some View { 5 | self.shadow(color: color.opacity(0.7), radius: 2.0, x: 2.0, y: 2.0) 6 | } 7 | 8 | func textShadow() -> some View { 9 | self.ghostEffectShadow(Color.white) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/EstimatedSize/EstimatedSize.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A protocol that allows us to gather an object's size by defining the expected size on an extension. 4 | protocol EstimatedSize { 5 | var projectedByteCount: Int { get } 6 | } 7 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/RichNotes/EstimatedSize+RichNote.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension RichNote: EstimatedSize { 4 | var projectedByteCount: Int { 5 | // Threw this in an array cause otherwise it's too large to type-check fast enough, of course... 6 | return [ 7 | self.id.projectedByteCount, 8 | self.createdAt.projectedByteCount, 9 | self.updatedAt.projectedByteCount, 10 | self.isSynchronized.projectedByteCount, 11 | self.title.projectedByteCount, 12 | self.text.projectedByteCount, 13 | self.attachedURL.projectedByteCount, 14 | self.tags.projectedByteCount, 15 | self.annotations.projectedByteCount, 16 | self.imageAttachment?.projectedByteCount ?? 0, 17 | ].reduce(0, { 18 | return $0 + $1 19 | }) 20 | } 21 | } 22 | 23 | extension String: EstimatedSize { 24 | var projectedByteCount: Int { 25 | self.utf8.count 26 | } 27 | } 28 | 29 | extension Bool: EstimatedSize { 30 | var projectedByteCount: Int { 31 | MemoryLayout.size 32 | } 33 | } 34 | 35 | extension Int: EstimatedSize { 36 | var projectedByteCount: Int { 37 | MemoryLayout.size 38 | } 39 | } 40 | 41 | extension Float: EstimatedSize { 42 | var projectedByteCount: Int { 43 | MemoryLayout.size 44 | } 45 | } 46 | 47 | extension URL: EstimatedSize { 48 | var projectedByteCount: Int { 49 | MemoryLayout.size 50 | } 51 | } 52 | 53 | extension Date: EstimatedSize { 54 | var projectedByteCount: Int { 55 | MemoryLayout.size 56 | } 57 | } 58 | 59 | extension Tag: EstimatedSize { 60 | var projectedByteCount: Int { 61 | self.title.projectedByteCount + 62 | self.color.projectedByteCount 63 | } 64 | } 65 | 66 | extension Annotation: EstimatedSize { 67 | var projectedByteCount: Int { 68 | self.text.projectedByteCount 69 | } 70 | } 71 | 72 | extension Image: EstimatedSize { 73 | var projectedByteCount: Int { 74 | self.url.projectedByteCount + 75 | self.width.projectedByteCount + 76 | self.height.projectedByteCount 77 | } 78 | } 79 | 80 | extension Collection where Element: EstimatedSize { 81 | var projectedByteCount: Int { 82 | self.reduce(0) { 83 | return $0 + $1.projectedByteCount 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/RichNotes/MemoryFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum MemoryFormatter { 4 | static func formatted(bytes: Int, unit: ByteCountFormatStyle.Units = .mb) -> String { 5 | ByteCountFormatStyle(style: .memory, allowedUnits: unit).format(Int64(bytes)) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/RichNotes/PerformanceStatisticItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct PerformanceStatisticItem: Identifiable { 4 | let title: String 5 | let measurement: String 6 | 7 | var id: UUID { 8 | UUID() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/RichNotes/RichNotesController.swift: -------------------------------------------------------------------------------- 1 | import Boutique 2 | import SwiftUI 3 | 4 | @Observable 5 | final class RichNotesController { 6 | @ObservationIgnored 7 | @Stored(in: .notesStore) var notes: [RichNote] 8 | 9 | init(store: Store) { 10 | self._notes = Stored(in: store) 11 | } 12 | 13 | func addItems(count: Int) async throws { 14 | do { 15 | var items = Array(repeating: RichNote.demoNote, count: count) 16 | 17 | // Adding N items by setting their id to UUIDs to ensure they are unique 18 | for (index, _) in zip(items.indices, items) { 19 | items[index].id = UUID().uuidString 20 | } 21 | 22 | // Profiling how fast the operation is, consider elevating this to the UI 23 | let timeBeforeAction = Date().timeIntervalSince1970 24 | 25 | try await self.$notes.insert(items) 26 | 27 | let timeAfterAction = Date().timeIntervalSince1970 28 | print(timeBeforeAction, timeAfterAction, String(format: "%.5fs", timeAfterAction - timeBeforeAction)) 29 | } catch { 30 | print(error) 31 | } 32 | } 33 | 34 | func removeItems(count: Int) async throws { 35 | let removalCount = min(self.notes.count, count) 36 | 37 | do { 38 | let firstElements = Array(self.notes[0..: DynamicProperty { 5 | @Environment(\.horizontalSizeClass) private var horizontalSizeClass 6 | @Environment(\.verticalSizeClass) private var verticalSizeClass 7 | 8 | var regular: T 9 | var compact: T 10 | 11 | var wrappedValue: T { 12 | return horizontalSizeClass == .regular && verticalSizeClass == .regular 13 | ? regular : compact 14 | } 15 | } 16 | 17 | extension EnvironmentValues { 18 | var isRegularSizeClass: Bool { 19 | horizontalSizeClass == .regular && verticalSizeClass == .regular 20 | } 21 | 22 | var isCompactSizeClass: Bool { 23 | horizontalSizeClass != .regular || verticalSizeClass != .regular 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Performance Profiler/Sources/SwiftUI/SizingResistantView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A wrapper to be used around buttons it's not their content size 4 | /// that determines how a V/HStack sizes the Views. 5 | struct SizingResistantView: View { 6 | var content: () -> Content 7 | 8 | init(@ViewBuilder content: @escaping () -> Content) { 9 | self.content = content 10 | } 11 | 12 | var body: some View { 13 | content() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Boutique/Internal/AsyncValueSubject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal final class AsyncValueSubject: @unchecked Sendable { 4 | typealias BufferingPolicy = AsyncStream.Continuation.BufferingPolicy 5 | 6 | private let lock = NSLock() 7 | 8 | var value: Value 9 | var bufferingPolicy: BufferingPolicy 10 | 11 | private var continuations: [UInt: AsyncStream.Continuation] = [:] 12 | private var count: UInt = 0 13 | 14 | public init(_ initialValue: Value, bufferingPolicy: BufferingPolicy = .unbounded) { 15 | self.value = initialValue 16 | self.bufferingPolicy = bufferingPolicy 17 | } 18 | 19 | // new mutex lock, but iOS 18 20 | // nslock or dispatchqueue 21 | 22 | func send(_ newValue: Value) { 23 | // Acquire lock before updating state. 24 | self.lock.lock() 25 | self.value = newValue 26 | // Copy continuations to avoid iterating while holding the lock. 27 | let currentContinuations = self.continuations 28 | self.lock.unlock() 29 | 30 | for (_, continuation) in currentContinuations { 31 | continuation.yield(newValue) 32 | } 33 | } 34 | 35 | func `inout`(_ apply: @Sendable (inout Value) -> Void) { 36 | self.lock.lock() 37 | apply(&value) 38 | // Capture current state and continuations. 39 | let currentValue = value 40 | let currentContinuations = continuations 41 | self.lock.unlock() 42 | 43 | for (_, continuation) in currentContinuations { 44 | continuation.yield(currentValue) 45 | } 46 | } 47 | 48 | var values: AsyncStream { 49 | AsyncStream(bufferingPolicy: self.bufferingPolicy) { continuation in 50 | self.insert(continuation) 51 | } 52 | } 53 | } 54 | 55 | private extension AsyncValueSubject { 56 | func insert(_ continuation: AsyncStream.Continuation) { 57 | self.lock.lock() 58 | continuation.yield(value) 59 | let id = count + 1 60 | count = id 61 | continuations[id] = continuation 62 | continuation.onTermination = { [weak self] _ in 63 | guard let self = self else { return } 64 | 65 | Task { self.remove(continuation: id) } 66 | } 67 | self.lock.unlock() 68 | } 69 | 70 | func remove(continuation id: UInt) { 71 | self.lock.lock() 72 | continuations.removeValue(forKey: id) 73 | self.lock.unlock() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Boutique/Internal/BoxedValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension JSONEncoder { 4 | func encodeBoxedData(item: Item) throws -> Data { 5 | return try JSONCoders.encoder.encode( 6 | BoxedValue(value: item) 7 | ) 8 | } 9 | } 10 | 11 | extension JSONDecoder { 12 | func decodeBoxedData(data: Data) throws -> Item { 13 | return try self.decode( 14 | BoxedValue.self, from: data 15 | ) 16 | .value 17 | } 18 | } 19 | 20 | struct BoxedValue: Codable { 21 | var value: T 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Boutique/Internal/CachedValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `CachedValue` exists internally for the purpose of creating a reference value, preventing the need 4 | /// to create a `JSONDecoder` and invoke a decode step every time we need to access a `StoredValue` externally. 5 | internal final class CachedValue { 6 | private var cachedValue: Item? 7 | public let retrieveValue: () -> Item 8 | 9 | init(retrieveValue: @escaping () -> Item) { 10 | self.retrieveValue = retrieveValue 11 | } 12 | 13 | func set(_ value: Item) { 14 | self.cachedValue = value 15 | } 16 | 17 | var wrappedValue: Item { 18 | if let cachedValue { 19 | return cachedValue 20 | } else { 21 | let retrievedValue = self.retrieveValue() 22 | self.cachedValue = retrievedValue 23 | return retrievedValue 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Boutique/Internal/JSONCoders.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // A set of Encoders/Decoders used across Boutique. 4 | // Rather than using different encoders/decoders across functions we can 5 | // allocate them once here and not face additional performance costs. 6 | enum JSONCoders { 7 | static let encoder = JSONEncoder() 8 | static let decoder = JSONDecoder() 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Boutique/Internal/Keychain.swift: -------------------------------------------------------------------------------- 1 | import Security 2 | 3 | // Inspired by Valet's KeychainError implementation: 4 | // https://github.com/square/Valet/blob/master/Sources/Valet/KeychainError.swift 5 | public enum KeychainError: Error { 6 | /// The keychain could not be accessed. 7 | case couldNotAccessKeychain 8 | 9 | /// No data was found for the requested key. 10 | case itemNotFound 11 | 12 | /// The application does not have the proper entitlements to perform the requested action. 13 | /// This may be due to an Apple Keychain bug. As a workaround try running on a device that is not attached to a debugger. 14 | /// - SeeAlso: https://forums.developer.apple.com/thread/4743 15 | case missingEntitlement 16 | 17 | /// We did not match any commonly encountered keychain errors and want to bubble up the status code 18 | case errorWithStatus(status: OSStatus) 19 | 20 | init(status: OSStatus) { 21 | switch status { 22 | 23 | case errSecInvalidAccessCredentials, errSecInvalidAccessRequest, errSecInvalidAttributeAccessCredentials, errSecMissingAttributeAccessCredentials, errSecNoAccessForItem: 24 | self = .couldNotAccessKeychain 25 | 26 | case errSecItemNotFound: 27 | self = .itemNotFound 28 | 29 | case errSecMissingEntitlement: 30 | self = .missingEntitlement 31 | 32 | default: 33 | self = .errorWithStatus(status: status) 34 | } 35 | } 36 | } 37 | 38 | /// A type representing Tagged, to statically represent the keychain's Service. 39 | /// This is done to be more type-safe than passing string parameters in all places. 40 | public struct KeychainService: ExpressibleByStringLiteral { 41 | public let value: String 42 | 43 | public init(value: String) { 44 | self.value = value 45 | } 46 | 47 | public init(stringLiteral value: StaticString) { 48 | self = KeychainService(value: "\(value)") 49 | } 50 | } 51 | 52 | /// A type representing Tagged, to statically represent the keychain's Group. 53 | /// This is done to be more type-safe than passing string parameters in all places. 54 | public struct KeychainGroup: ExpressibleByStringLiteral { 55 | public let value: String 56 | 57 | public init(value: String) { 58 | self.value = value 59 | } 60 | 61 | public init(stringLiteral value: StaticString) { 62 | self = KeychainGroup(value: "\(value)") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Boutique/Internal/Store.ItemRemovalStrategy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Store { 4 | /// An invalidation strategy for a `Store` instance. 5 | /// 6 | /// An `ItemRemovalStrategy` provides control over how items are removed from the `Store` 7 | /// and `StorageEngine` cache when you are inserting new items into the `Store`. 8 | /// 9 | /// This type used to be used publicly but now it's only used internally. As a result you 10 | /// can no longer construct your own strategies, only `.all` and `.items(_:)` remain. 11 | 12 | struct ItemRemovalStrategy { 13 | public init(removedItems: @escaping ([RemovedItem]) -> [RemovedItem]) { self.removedItems = removedItems } 14 | 15 | public var removedItems: ([RemovedItem]) -> [RemovedItem] 16 | 17 | /// Removes all of the items from the in-memory and the StorageEngine cache before saving new items. 18 | internal static var all: ItemRemovalStrategy { 19 | ItemRemovalStrategy(removedItems: { $0 }) 20 | } 21 | 22 | /// Removes the specific items you provide from the `Store` and disk cache before saving new items. 23 | /// 24 | /// - Parameter itemsToRemove: The items being removed. 25 | /// - Returns: A `ItemRemovalStrategy` where the items provided are removed 26 | /// from the `Store` and disk cache before saving new items. 27 | internal static func items(_ itemsToRemove: [RemovedItem]) -> ItemRemovalStrategy { 28 | ItemRemovalStrategy(removedItems: { _ in itemsToRemove }) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Boutique/StorableItem.swift: -------------------------------------------------------------------------------- 1 | public typealias StorableItem = Codable & Sendable 2 | -------------------------------------------------------------------------------- /Sources/Boutique/Store+Observation.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension View { 4 | func onStoreDidLoad(_ store: Store, onLoad: @escaping () -> Void, onError: ((Error) -> Void)? = nil) -> some View { 5 | self.task({ 6 | do { 7 | try await store.itemsHaveLoaded() 8 | onLoad() 9 | } catch { 10 | onError?(error) 11 | } 12 | }) 13 | } 14 | 15 | func onStoreDidLoad(_ store: Store, update hasLoadedState: Binding, onError: ((Error) -> Void)? = nil) -> some View { 16 | self.task({ 17 | do { 18 | try await store.itemsHaveLoaded() 19 | hasLoadedState.wrappedValue = true 20 | } catch { 21 | onError?(error) 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Boutique/StoreEvent.swift: -------------------------------------------------------------------------------- 1 | /// An event associated with an operation performed on a ``Store``. 2 | /// 3 | /// `StoreEvent` provides a way to observe specific operations that occur on a ``Store``, 4 | /// including when it's initialized, loaded, and when items are inserted or removed. 5 | /// Each event includes the operation type and the items affected by that operation. 6 | /// 7 | /// You can observe these events using the ``Store/events`` property, like so: 8 | /// 9 | /// ```swift 10 | /// func monitorStoreEvents() async { 11 | /// for await event in store.events { 12 | /// switch event.operation { 13 | /// case .initialized: 14 | /// print("Store has initialized") 15 | /// case .loaded: 16 | /// print("Store has loaded with items", event.items) 17 | /// case .insert: 18 | /// print("Store inserted items", event.items) 19 | /// case .remove: 20 | /// print("Store removed items", event.items) 21 | /// } 22 | /// } 23 | /// } 24 | /// ``` 25 | public struct StoreEvent: StorableItem { 26 | /// The type of operation that occurred on the ``Store`` through a ``StoreEvent``. 27 | public let operation: Operation 28 | 29 | /// The items affected by the operation. 30 | /// For `.initialized`, this will be an empty array. 31 | /// For `.loaded`, this will contain all items loaded from storage. 32 | /// For `.insert`, this will contain the newly inserted items. 33 | /// For `.remove`, this will contain the removed items. 34 | public let items: [Item] 35 | 36 | /// The type of operation that can occur on a ``Store``. 37 | public enum Operation: StorableItem { 38 | /// The ``Store`` has been initialized but items have not yet been loaded. 39 | case initialized 40 | 41 | /// The ``Store`` has loaded its items from storage. 42 | case loaded 43 | 44 | /// Items have been inserted into the ``Store``. 45 | case insert 46 | 47 | /// Items have been removed from the ``Store``. 48 | case remove 49 | } 50 | } 51 | 52 | internal extension StoreEvent { 53 | static var initial: StoreEvent { 54 | StoreEvent(operation: .initialized, items: []) 55 | } 56 | 57 | static func loaded(_ items: [Item]) -> StoreEvent { 58 | StoreEvent(operation: .loaded, items: items) 59 | } 60 | 61 | static func insert(_ items: [Item]) -> StoreEvent { 62 | StoreEvent(operation: .insert, items: items) 63 | } 64 | 65 | static func remove(_ items: [Item]) -> StoreEvent { 66 | StoreEvent(operation: .remove, items: items) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Boutique/Stored.swift: -------------------------------------------------------------------------------- 1 | import Observation 2 | 3 | /// The @``Stored`` property wrapper to automagically initialize a ``Store``. 4 | @MainActor 5 | /// 6 | /// When using `@Stored` in an `@Observable` class, you should add the `@ObservationIgnored` attribute 7 | /// to prevent duplicate observation tracking: 8 | /// 9 | /// ```swift 10 | /// @Observable 11 | /// final class NotesController { 12 | /// @ObservationIgnored 13 | /// @Stored var notes: [Note] 14 | /// 15 | /// init(store: Store) { 16 | /// self._notes = Stored(in: store) 17 | /// } 18 | /// 19 | /// func addNote(note: Note) async throws { 20 | /// try await self.$notes.insert(note) 21 | /// } 22 | /// } 23 | /// ``` 24 | /// 25 | /// You can observe changes to the stored items using SwiftUI's `.onChange` modifier: 26 | /// 27 | /// ```swift 28 | /// .onChange(of: notesController.notes, initial: true) { _, newValue in 29 | /// self.filteredNotes = newValue.filter { $0.isImportant } 30 | /// } 31 | /// ``` 32 | @propertyWrapper 33 | public struct Stored { 34 | private let store: Store 35 | 36 | /// Initializes a @``Stored`` property that will be exposed as an `[Item]` and project a `Store`. 37 | /// - Parameter store: The store that will be wrapped to expose as an array. 38 | public init(in store: Store) { 39 | self.store = store 40 | } 41 | 42 | /// The currently stored items 43 | public var wrappedValue: [Item] { 44 | self.store.items 45 | } 46 | 47 | public var projectedValue: Store { 48 | self.store 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Boutique/StoredValue+Binding.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension StoredValue { 4 | /// A convenient way to create a `Binding` from a `StoredValue`. 5 | /// 6 | /// - Returns: A `Binding` of the `StoredValue` provided. 7 | var binding: Binding { 8 | Binding(get: { 9 | self.wrappedValue 10 | }, set: { 11 | self.projectedValue.set($0) 12 | }) 13 | } 14 | } 15 | 16 | public extension SecurelyStoredValue { 17 | /// A convenient way to create a `Binding` from a `SecurelyStoredValue`. 18 | /// 19 | /// - Returns: A `Binding` of the `SecurelyStoredValue` provided. 20 | var binding: Binding { 21 | Binding(get: { 22 | self.wrappedValue 23 | }, set: { 24 | try? self.projectedValue.set($0) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Boutique/StoredValue+Bool.swift: -------------------------------------------------------------------------------- 1 | public extension StoredValue where Item == Bool { 2 | /// A function to toggle an @``StoredValue`` that represent a `Bool`. 3 | /// 4 | /// This is meant to provide a simple ergonomic improvement, avoiding callsites like this. 5 | /// ``` 6 | /// self.appState.$proFeaturesEnabled.set(!self.appState.proFeaturesEnabled) 7 | /// ``` 8 | /// 9 | /// Instead having a much simpler simpler option. 10 | /// ``` 11 | /// self.appState.$proFeaturesEnabled.toggle() 12 | /// ``` 13 | func toggle() { 14 | self.set(!self.wrappedValue) 15 | } 16 | } 17 | 18 | public extension SecurelyStoredValue where Item == Bool { 19 | /// A function to toggle a @``SecurelyStoredValue`` that represent a `Bool`. 20 | /// 21 | /// This is meant to provide a simple ergonomic improvement, avoiding callsites like this. 22 | /// ``` 23 | /// try self.appState.$isLoggedIn.set(!self.appState.proFeaturesEnabled) 24 | /// ``` 25 | /// 26 | /// Instead having a much simpler simpler option. 27 | /// ``` 28 | /// try self.appState.$isLoggedIn.toggle() 29 | /// ``` 30 | func toggle() throws { 31 | if let wrappedValue { 32 | try self.set(!wrappedValue) 33 | } else { 34 | throw KeychainError.couldNotAccessKeychain 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Sources/Boutique/StoredValue+Dictionary.swift: -------------------------------------------------------------------------------- 1 | public extension StoredValue { 2 | /// A function to set a @``StoredValue`` represented by a `Dictionary` 3 | /// without having to manually make an intermediate copy for every value update. 4 | /// 5 | /// This is meant to provide a simple ergonomic improvement, avoiding callsites like this. 6 | /// ``` 7 | /// var updatedRedPandaList = self.redPandaList 8 | /// updatedRedPandaList["best"] = "Pabu" 9 | /// self.$redPandaList.set(updatedRedPandaList) 10 | /// ``` 11 | /// 12 | /// Instead this function provides a much simpler alternative. 13 | /// ``` 14 | /// try await self.$redPandaList.update(key: "best", value: "Pabu") 15 | /// ``` 16 | func update(key: Key, value: Value?) where Item == [Key: Value] { 17 | var updatedDictionary = self.wrappedValue 18 | updatedDictionary[key] = value 19 | self.set(updatedDictionary) 20 | } 21 | } 22 | 23 | public extension SecurelyStoredValue { 24 | /// A function to set a @``SecurelyStoredValue`` represented by a `Dictionary` 25 | /// without having to manually make an intermediate copy for every value update. 26 | /// 27 | /// This is meant to provide a simple ergonomic improvement, avoiding callsites like this. 28 | /// ``` 29 | /// var updatedRedPandaList = self.redPandaList 30 | /// updatedRedPandaList["best"] = "Pabu" 31 | /// self.$redPandaList.set(updatedRedPandaList) 32 | /// ``` 33 | /// 34 | /// Instead this function provides a much simpler alternative. 35 | /// ``` 36 | /// try await self.$redPandaList.update(key: "best", value: "Pabu") 37 | /// ``` 38 | /// To better match expected uses calling update on a currently nil SecurelyStoredValue 39 | /// will return a single element dictionary of the passed in key/value, 40 | /// rather than returning nil or throwing an error. 41 | func update(key: Key, value: Value?) throws where Item == [Key: Value] { 42 | var updatedDictionary = self.wrappedValue ?? [:] 43 | updatedDictionary[key] = value 44 | try self.set(updatedDictionary) 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /Sources/Boutique/StoredValue+KeypathSetter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension StoredValue { 4 | /// A function to set the value of a property inside of a @``StoredValue`` object 5 | /// without having to manually make an intermediate copy for every value update. 6 | /// 7 | /// This is meant to provide a simple ergonomic improvement for complex objects, 8 | /// avoiding callsites like this. 9 | /// 10 | /// ``` 11 | /// struct 3DCoordinates: Codable { 12 | /// let x: Double 13 | /// let y: Double 14 | /// let z: Double 15 | /// } 16 | /// 17 | /// var coordinates = self.coordinates 18 | /// coordinates.x = 1.0 19 | /// self.$coordinates.set(coordinates) 20 | /// ``` 21 | /// 22 | /// Instead this function provides a much simpler alternative. 23 | /// ``` 24 | /// self.$coordinates.set(\.x, to: 1.0) 25 | /// ``` 26 | public func set(_ keyPath: WritableKeyPath, to value: Value) { 27 | var updatedValue = self.wrappedValue 28 | updatedValue[keyPath: keyPath] = value 29 | self.set(updatedValue) 30 | } 31 | } 32 | 33 | extension SecurelyStoredValue { 34 | /// A function to set the value of a property inside of a @``StoredValue`` object 35 | /// without having to manually make an intermediate copy for every value update. 36 | /// 37 | /// This is meant to provide a simple ergonomic improvement for complex objects, 38 | /// avoiding callsites like this. 39 | /// 40 | /// ``` 41 | /// struct 3DCoordinates: Codable { 42 | /// let x: Double 43 | /// let y: Double 44 | /// let z: Double 45 | /// } 46 | /// 47 | /// var coordinates = self.coordinates 48 | /// coordinates.x = 1.0 49 | /// try self.$coordinates.set(coordinates) 50 | /// ``` 51 | /// 52 | /// Instead this function provides a much simpler alternative. 53 | /// ``` 54 | /// try self.$coordinates.set(\.x, to: 1.0) 55 | /// ``` 56 | public func set(_ keyPath: WritableKeyPath, to value: Value) throws { 57 | if let wrappedValue { 58 | var updatedValue = wrappedValue 59 | updatedValue[keyPath: keyPath] = value 60 | try self.set(updatedValue) 61 | } else { 62 | throw KeychainError.couldNotAccessKeychain 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/BoutiqueTests/BoutiqueItem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct BoutiqueItem: Codable, Sendable, Equatable, Identifiable { 4 | var id: String { 5 | self.merchantID 6 | } 7 | 8 | let merchantID: String 9 | let value: String 10 | } 11 | 12 | extension BoutiqueItem { 13 | static let coat = BoutiqueItem( 14 | merchantID: "1", 15 | value: "Coat" 16 | ) 17 | 18 | static let sweater = BoutiqueItem( 19 | merchantID: "2", 20 | value: "Sweater" 21 | ) 22 | 23 | static let purse = BoutiqueItem( 24 | merchantID: "3", 25 | value: "Purse" 26 | ) 27 | 28 | static let belt = BoutiqueItem( 29 | merchantID: "4", 30 | value: "Belt" 31 | ) 32 | 33 | static let duplicateBelt = BoutiqueItem( 34 | merchantID: "4", 35 | value: "Belt" 36 | ) 37 | } 38 | 39 | extension [BoutiqueItem] { 40 | static let allItems: [BoutiqueItem] = [ 41 | .coat, 42 | .sweater, 43 | .purse, 44 | .belt, 45 | .duplicateBelt 46 | ] 47 | 48 | static let uniqueItems: [BoutiqueItem] = [ 49 | .coat, 50 | .sweater, 51 | .purse, 52 | .belt, 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /Tests/BoutiqueTests/StoreEvent.Tests.Requirements.swift: -------------------------------------------------------------------------------- 1 | import Boutique 2 | import Testing 3 | 4 | extension Store { 5 | func validateStoreEvent(event: StoreEvent) throws { 6 | if self.items.isEmpty { 7 | try #require(event.operation == .initialized || event.operation == .loaded || event.operation == .remove) 8 | } else { 9 | try #require(event.operation == .insert) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/data/documentation/boutique/keychainerror/couldnotaccesskeychain.json: -------------------------------------------------------------------------------- 1 | {"abstract":[{"type":"text","text":"The keychain could not be accessed."}],"metadata":{"modules":[{"name":"Boutique"}],"roleHeading":"Case","title":"KeychainError.couldNotAccessKeychain","role":"symbol","fragments":[{"text":"case","kind":"keyword"},{"kind":"text","text":" "},{"kind":"identifier","text":"couldNotAccessKeychain"}],"externalID":"s:8Boutique13KeychainErrorO014couldNotAccessB0yA2CmF","symbolKind":"case"},"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainError\/couldNotAccessKeychain"},"kind":"symbol","primaryContentSections":[{"declarations":[{"tokens":[{"kind":"keyword","text":"case"},{"text":" ","kind":"text"},{"kind":"identifier","text":"couldNotAccessKeychain"}],"languages":["swift"],"platforms":["macOS"]}],"kind":"declarations"}],"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/boutique\/keychainerror\/couldnotaccesskeychain"]}],"schemaVersion":{"patch":0,"major":0,"minor":3},"sections":[],"hierarchy":{"paths":[["doc:\/\/Boutique\/documentation\/Boutique","doc:\/\/Boutique\/documentation\/Boutique\/KeychainError"]]},"references":{"doc://Boutique/documentation/Boutique/KeychainError":{"fragments":[{"text":"enum","kind":"keyword"},{"text":" ","kind":"text"},{"text":"KeychainError","kind":"identifier"}],"kind":"symbol","role":"symbol","navigatorTitle":[{"kind":"identifier","text":"KeychainError"}],"abstract":[],"title":"KeychainError","type":"topic","url":"\/documentation\/boutique\/keychainerror","identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainError"},"doc://Boutique/documentation/Boutique":{"kind":"symbol","url":"\/documentation\/boutique","abstract":[{"text":"A simple but surprisingly fancy data store and so much more","type":"text"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique","role":"collection","title":"Boutique","type":"topic"},"doc://Boutique/documentation/Boutique/KeychainError/couldNotAccessKeychain":{"kind":"symbol","fragments":[{"text":"case","kind":"keyword"},{"kind":"text","text":" "},{"text":"couldNotAccessKeychain","kind":"identifier"}],"abstract":[{"type":"text","text":"The keychain could not be accessed."}],"title":"KeychainError.couldNotAccessKeychain","url":"\/documentation\/boutique\/keychainerror\/couldnotaccesskeychain","identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainError\/couldNotAccessKeychain","type":"topic","role":"symbol"}}} -------------------------------------------------------------------------------- /docs/data/documentation/boutique/keychainerror/error-implementations.json: -------------------------------------------------------------------------------- 1 | {"schemaVersion":{"major":0,"minor":3,"patch":0},"sections":[],"hierarchy":{"paths":[["doc:\/\/Boutique\/documentation\/Boutique","doc:\/\/Boutique\/documentation\/Boutique\/KeychainError"]]},"kind":"article","metadata":{"roleHeading":"API Collection","modules":[{"name":"Boutique"}],"title":"Error Implementations","role":"collectionGroup"},"topicSections":[{"identifiers":["doc:\/\/Boutique\/documentation\/Boutique\/KeychainError\/localizedDescription"],"generated":true,"title":"Instance Properties"}],"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/boutique\/keychainerror\/error-implementations"]}],"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainError\/Error-Implementations"},"references":{"doc://Boutique/documentation/Boutique":{"kind":"symbol","url":"\/documentation\/boutique","abstract":[{"text":"A simple but surprisingly fancy data store and so much more","type":"text"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique","role":"collection","title":"Boutique","type":"topic"},"doc://Boutique/documentation/Boutique/KeychainError":{"fragments":[{"text":"enum","kind":"keyword"},{"text":" ","kind":"text"},{"text":"KeychainError","kind":"identifier"}],"kind":"symbol","role":"symbol","navigatorTitle":[{"kind":"identifier","text":"KeychainError"}],"abstract":[],"title":"KeychainError","type":"topic","url":"\/documentation\/boutique\/keychainerror","identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainError"},"doc://Boutique/documentation/Boutique/KeychainError/localizedDescription":{"abstract":[],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainError\/localizedDescription","kind":"symbol","type":"topic","role":"symbol","title":"localizedDescription","url":"\/documentation\/boutique\/keychainerror\/localizeddescription","fragments":[{"kind":"keyword","text":"var"},{"text":" ","kind":"text"},{"text":"localizedDescription","kind":"identifier"},{"kind":"text","text":": "},{"kind":"typeIdentifier","preciseIdentifier":"s:SS","text":"String"}]}}} -------------------------------------------------------------------------------- /docs/data/documentation/boutique/keychainerror/itemnotfound.json: -------------------------------------------------------------------------------- 1 | {"metadata":{"role":"symbol","fragments":[{"text":"case","kind":"keyword"},{"kind":"text","text":" "},{"text":"itemNotFound","kind":"identifier"}],"title":"KeychainError.itemNotFound","symbolKind":"case","externalID":"s:8Boutique13KeychainErrorO12itemNotFoundyA2CmF","roleHeading":"Case","modules":[{"name":"Boutique"}]},"identifier":{"url":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainError\/itemNotFound","interfaceLanguage":"swift"},"sections":[],"kind":"symbol","primaryContentSections":[{"declarations":[{"platforms":["macOS"],"tokens":[{"kind":"keyword","text":"case"},{"text":" ","kind":"text"},{"text":"itemNotFound","kind":"identifier"}],"languages":["swift"]}],"kind":"declarations"}],"variants":[{"paths":["\/documentation\/boutique\/keychainerror\/itemnotfound"],"traits":[{"interfaceLanguage":"swift"}]}],"schemaVersion":{"major":0,"patch":0,"minor":3},"hierarchy":{"paths":[["doc:\/\/Boutique\/documentation\/Boutique","doc:\/\/Boutique\/documentation\/Boutique\/KeychainError"]]},"abstract":[{"type":"text","text":"No data was found for the requested key."}],"references":{"doc://Boutique/documentation/Boutique/KeychainError":{"fragments":[{"text":"enum","kind":"keyword"},{"text":" ","kind":"text"},{"text":"KeychainError","kind":"identifier"}],"kind":"symbol","role":"symbol","navigatorTitle":[{"kind":"identifier","text":"KeychainError"}],"abstract":[],"title":"KeychainError","type":"topic","url":"\/documentation\/boutique\/keychainerror","identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainError"},"doc://Boutique/documentation/Boutique/KeychainError/itemNotFound":{"fragments":[{"kind":"keyword","text":"case"},{"text":" ","kind":"text"},{"text":"itemNotFound","kind":"identifier"}],"kind":"symbol","role":"symbol","abstract":[{"type":"text","text":"No data was found for the requested key."}],"title":"KeychainError.itemNotFound","type":"topic","url":"\/documentation\/boutique\/keychainerror\/itemnotfound","identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainError\/itemNotFound"},"doc://Boutique/documentation/Boutique":{"kind":"symbol","url":"\/documentation\/boutique","abstract":[{"text":"A simple but surprisingly fancy data store and so much more","type":"text"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique","role":"collection","title":"Boutique","type":"topic"}}} -------------------------------------------------------------------------------- /docs/data/documentation/boutique/keychaingroup/init(value:).json: -------------------------------------------------------------------------------- 1 | {"schemaVersion":{"minor":3,"patch":0,"major":0},"kind":"symbol","identifier":{"interfaceLanguage":"swift","url":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainGroup\/init(value:)"},"sections":[],"metadata":{"modules":[{"name":"Boutique"}],"title":"init(value:)","externalID":"s:8Boutique13KeychainGroupV5valueACSS_tcfc","role":"symbol","fragments":[{"text":"init","kind":"identifier"},{"kind":"text","text":"("},{"text":"value","kind":"externalParam"},{"kind":"text","text":": "},{"text":"String","preciseIdentifier":"s:SS","kind":"typeIdentifier"},{"text":")","kind":"text"}],"symbolKind":"init","roleHeading":"Initializer"},"primaryContentSections":[{"kind":"declarations","declarations":[{"platforms":["macOS"],"languages":["swift"],"tokens":[{"text":"init","kind":"keyword"},{"kind":"text","text":"("},{"text":"value","kind":"externalParam"},{"text":": ","kind":"text"},{"kind":"typeIdentifier","preciseIdentifier":"s:SS","text":"String"},{"text":")","kind":"text"}]}]}],"hierarchy":{"paths":[["doc:\/\/Boutique\/documentation\/Boutique","doc:\/\/Boutique\/documentation\/Boutique\/KeychainGroup"]]},"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/boutique\/keychaingroup\/init(value:)"]}],"references":{"doc://Boutique/documentation/Boutique":{"kind":"symbol","url":"\/documentation\/boutique","abstract":[{"text":"A simple but surprisingly fancy data store and so much more","type":"text"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique","role":"collection","title":"Boutique","type":"topic"},"doc://Boutique/documentation/Boutique/KeychainGroup/init(value:)":{"kind":"symbol","abstract":[],"role":"symbol","fragments":[{"kind":"identifier","text":"init"},{"text":"(","kind":"text"},{"kind":"externalParam","text":"value"},{"kind":"text","text":": "},{"kind":"typeIdentifier","preciseIdentifier":"s:SS","text":"String"},{"text":")","kind":"text"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainGroup\/init(value:)","url":"\/documentation\/boutique\/keychaingroup\/init(value:)","type":"topic","title":"init(value:)"},"doc://Boutique/documentation/Boutique/KeychainGroup":{"navigatorTitle":[{"kind":"identifier","text":"KeychainGroup"}],"kind":"symbol","type":"topic","url":"\/documentation\/boutique\/keychaingroup","title":"KeychainGroup","abstract":[{"text":"A type representing Tagged","type":"text"},{"text":", to statically represent the keychain’s Group.","type":"text"},{"type":"text","text":" "},{"text":"This is done to be more type-safe than passing string parameters in all places.","type":"text"}],"role":"symbol","fragments":[{"text":"struct","kind":"keyword"},{"kind":"text","text":" "},{"text":"KeychainGroup","kind":"identifier"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainGroup"}}} -------------------------------------------------------------------------------- /docs/data/documentation/boutique/keychaingroup/value.json: -------------------------------------------------------------------------------- 1 | {"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainGroup\/value"},"schemaVersion":{"patch":0,"major":0,"minor":3},"kind":"symbol","primaryContentSections":[{"declarations":[{"platforms":["macOS"],"languages":["swift"],"tokens":[{"kind":"keyword","text":"let"},{"kind":"text","text":" "},{"text":"value","kind":"identifier"},{"text":": ","kind":"text"},{"text":"String","kind":"typeIdentifier","preciseIdentifier":"s:SS"}]}],"kind":"declarations"}],"hierarchy":{"paths":[["doc:\/\/Boutique\/documentation\/Boutique","doc:\/\/Boutique\/documentation\/Boutique\/KeychainGroup"]]},"sections":[],"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/boutique\/keychaingroup\/value"]}],"metadata":{"modules":[{"name":"Boutique"}],"roleHeading":"Instance Property","role":"symbol","symbolKind":"property","title":"value","externalID":"s:8Boutique13KeychainGroupV5valueSSvp","fragments":[{"kind":"keyword","text":"let"},{"text":" ","kind":"text"},{"text":"value","kind":"identifier"},{"kind":"text","text":": "},{"kind":"typeIdentifier","text":"String","preciseIdentifier":"s:SS"}]},"references":{"doc://Boutique/documentation/Boutique/KeychainGroup":{"navigatorTitle":[{"kind":"identifier","text":"KeychainGroup"}],"kind":"symbol","type":"topic","url":"\/documentation\/boutique\/keychaingroup","title":"KeychainGroup","abstract":[{"text":"A type representing Tagged","type":"text"},{"text":", to statically represent the keychain’s Group.","type":"text"},{"type":"text","text":" "},{"text":"This is done to be more type-safe than passing string parameters in all places.","type":"text"}],"role":"symbol","fragments":[{"text":"struct","kind":"keyword"},{"kind":"text","text":" "},{"text":"KeychainGroup","kind":"identifier"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainGroup"},"doc://Boutique/documentation/Boutique/KeychainGroup/value":{"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainGroup\/value","kind":"symbol","type":"topic","abstract":[],"role":"symbol","title":"value","url":"\/documentation\/boutique\/keychaingroup\/value","fragments":[{"kind":"keyword","text":"let"},{"kind":"text","text":" "},{"text":"value","kind":"identifier"},{"text":": ","kind":"text"},{"preciseIdentifier":"s:SS","kind":"typeIdentifier","text":"String"}]},"doc://Boutique/documentation/Boutique":{"kind":"symbol","url":"\/documentation\/boutique","abstract":[{"text":"A simple but surprisingly fancy data store and so much more","type":"text"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique","role":"collection","title":"Boutique","type":"topic"}}} -------------------------------------------------------------------------------- /docs/data/documentation/boutique/keychainservice/init(value:).json: -------------------------------------------------------------------------------- 1 | {"identifier":{"url":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainService\/init(value:)","interfaceLanguage":"swift"},"metadata":{"role":"symbol","modules":[{"name":"Boutique"}],"fragments":[{"text":"init","kind":"identifier"},{"text":"(","kind":"text"},{"text":"value","kind":"externalParam"},{"kind":"text","text":": "},{"preciseIdentifier":"s:SS","kind":"typeIdentifier","text":"String"},{"text":")","kind":"text"}],"symbolKind":"init","roleHeading":"Initializer","externalID":"s:8Boutique15KeychainServiceV5valueACSS_tcfc","title":"init(value:)"},"primaryContentSections":[{"kind":"declarations","declarations":[{"tokens":[{"kind":"keyword","text":"init"},{"text":"(","kind":"text"},{"text":"value","kind":"externalParam"},{"text":": ","kind":"text"},{"text":"String","kind":"typeIdentifier","preciseIdentifier":"s:SS"},{"kind":"text","text":")"}],"languages":["swift"],"platforms":["macOS"]}]}],"variants":[{"paths":["\/documentation\/boutique\/keychainservice\/init(value:)"],"traits":[{"interfaceLanguage":"swift"}]}],"schemaVersion":{"major":0,"minor":3,"patch":0},"sections":[],"hierarchy":{"paths":[["doc:\/\/Boutique\/documentation\/Boutique","doc:\/\/Boutique\/documentation\/Boutique\/KeychainService"]]},"kind":"symbol","references":{"doc://Boutique/documentation/Boutique/KeychainService":{"navigatorTitle":[{"kind":"identifier","text":"KeychainService"}],"kind":"symbol","type":"topic","url":"\/documentation\/boutique\/keychainservice","title":"KeychainService","abstract":[{"text":"A type representing Tagged","type":"text"},{"type":"text","text":", to statically represent the keychain’s Service."},{"text":" ","type":"text"},{"type":"text","text":"This is done to be more type-safe than passing string parameters in all places."}],"role":"symbol","fragments":[{"text":"struct","kind":"keyword"},{"text":" ","kind":"text"},{"text":"KeychainService","kind":"identifier"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainService"},"doc://Boutique/documentation/Boutique":{"kind":"symbol","url":"\/documentation\/boutique","abstract":[{"text":"A simple but surprisingly fancy data store and so much more","type":"text"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique","role":"collection","title":"Boutique","type":"topic"},"doc://Boutique/documentation/Boutique/KeychainService/init(value:)":{"kind":"symbol","abstract":[],"role":"symbol","fragments":[{"text":"init","kind":"identifier"},{"kind":"text","text":"("},{"text":"value","kind":"externalParam"},{"text":": ","kind":"text"},{"preciseIdentifier":"s:SS","kind":"typeIdentifier","text":"String"},{"text":")","kind":"text"}],"url":"\/documentation\/boutique\/keychainservice\/init(value:)","identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainService\/init(value:)","type":"topic","title":"init(value:)"}}} -------------------------------------------------------------------------------- /docs/data/documentation/boutique/keychainservice/value.json: -------------------------------------------------------------------------------- 1 | {"primaryContentSections":[{"kind":"declarations","declarations":[{"platforms":["macOS"],"tokens":[{"kind":"keyword","text":"let"},{"kind":"text","text":" "},{"text":"value","kind":"identifier"},{"text":": ","kind":"text"},{"text":"String","preciseIdentifier":"s:SS","kind":"typeIdentifier"}],"languages":["swift"]}]}],"schemaVersion":{"patch":0,"minor":3,"major":0},"metadata":{"title":"value","roleHeading":"Instance Property","fragments":[{"kind":"keyword","text":"let"},{"text":" ","kind":"text"},{"text":"value","kind":"identifier"},{"text":": ","kind":"text"},{"text":"String","kind":"typeIdentifier","preciseIdentifier":"s:SS"}],"modules":[{"name":"Boutique"}],"externalID":"s:8Boutique15KeychainServiceV5valueSSvp","symbolKind":"property","role":"symbol"},"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/boutique\/keychainservice\/value"]}],"identifier":{"interfaceLanguage":"swift","url":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainService\/value"},"kind":"symbol","hierarchy":{"paths":[["doc:\/\/Boutique\/documentation\/Boutique","doc:\/\/Boutique\/documentation\/Boutique\/KeychainService"]]},"sections":[],"references":{"doc://Boutique/documentation/Boutique":{"kind":"symbol","url":"\/documentation\/boutique","abstract":[{"text":"A simple but surprisingly fancy data store and so much more","type":"text"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique","role":"collection","title":"Boutique","type":"topic"},"doc://Boutique/documentation/Boutique/KeychainService":{"navigatorTitle":[{"kind":"identifier","text":"KeychainService"}],"kind":"symbol","type":"topic","url":"\/documentation\/boutique\/keychainservice","title":"KeychainService","abstract":[{"text":"A type representing Tagged","type":"text"},{"type":"text","text":", to statically represent the keychain’s Service."},{"text":" ","type":"text"},{"type":"text","text":"This is done to be more type-safe than passing string parameters in all places."}],"role":"symbol","fragments":[{"text":"struct","kind":"keyword"},{"text":" ","kind":"text"},{"text":"KeychainService","kind":"identifier"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainService"},"doc://Boutique/documentation/Boutique/KeychainService/value":{"title":"value","identifier":"doc:\/\/Boutique\/documentation\/Boutique\/KeychainService\/value","role":"symbol","fragments":[{"kind":"keyword","text":"let"},{"kind":"text","text":" "},{"text":"value","kind":"identifier"},{"text":": ","kind":"text"},{"text":"String","preciseIdentifier":"s:SS","kind":"typeIdentifier"}],"abstract":[],"url":"\/documentation\/boutique\/keychainservice\/value","type":"topic","kind":"symbol"}}} -------------------------------------------------------------------------------- /docs/data/documentation/boutique/storableitem.json: -------------------------------------------------------------------------------- 1 | {"schemaVersion":{"patch":0,"minor":3,"major":0},"sections":[],"identifier":{"url":"doc:\/\/Boutique\/documentation\/Boutique\/StorableItem","interfaceLanguage":"swift"},"kind":"symbol","primaryContentSections":[{"declarations":[{"platforms":["macOS"],"tokens":[{"kind":"keyword","text":"typealias"},{"text":" ","kind":"text"},{"kind":"identifier","text":"StorableItem"},{"text":" = ","kind":"text"},{"text":"Codable","preciseIdentifier":"s:s7Codablea","kind":"typeIdentifier"},{"kind":"text","text":" & "},{"preciseIdentifier":"s:s8SendableP","text":"Sendable","kind":"typeIdentifier"}],"languages":["swift"]}],"kind":"declarations"}],"hierarchy":{"paths":[["doc:\/\/Boutique\/documentation\/Boutique"]]},"metadata":{"externalID":"s:8Boutique12StorableItema","symbolKind":"typealias","roleHeading":"Type Alias","navigatorTitle":[{"text":"StorableItem","kind":"identifier"}],"role":"symbol","modules":[{"name":"Boutique"}],"fragments":[{"text":"typealias","kind":"keyword"},{"kind":"text","text":" "},{"kind":"identifier","text":"StorableItem"}],"title":"StorableItem"},"variants":[{"traits":[{"interfaceLanguage":"swift"}],"paths":["\/documentation\/boutique\/storableitem"]}],"references":{"doc://Boutique/documentation/Boutique":{"kind":"symbol","url":"\/documentation\/boutique","abstract":[{"text":"A simple but surprisingly fancy data store and so much more","type":"text"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique","role":"collection","title":"Boutique","type":"topic"},"doc://Boutique/documentation/Boutique/StorableItem":{"role":"symbol","abstract":[],"fragments":[{"text":"typealias","kind":"keyword"},{"text":" ","kind":"text"},{"text":"StorableItem","kind":"identifier"}],"kind":"symbol","title":"StorableItem","navigatorTitle":[{"kind":"identifier","text":"StorableItem"}],"url":"\/documentation\/boutique\/storableitem","identifier":"doc:\/\/Boutique\/documentation\/Boutique\/StorableItem","type":"topic"}}} -------------------------------------------------------------------------------- /docs/developer-og-twitter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/docs/developer-og-twitter.jpg -------------------------------------------------------------------------------- /docs/developer-og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/docs/developer-og.jpg -------------------------------------------------------------------------------- /docs/documentation/boutique/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainerror/couldnotaccesskeychain/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainerror/error-implementations/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainerror/errorwithstatus(status:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainerror/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainerror/itemnotfound/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainerror/localizeddescription/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainerror/missingentitlement/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychaingroup/expressiblebyextendedgraphemeclusterliteral-implementations/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychaingroup/expressiblebyunicodescalarliteral-implementations/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychaingroup/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychaingroup/init(extendedgraphemeclusterliteral:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychaingroup/init(stringliteral:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychaingroup/init(unicodescalarliteral:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychaingroup/init(value:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychaingroup/value/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainservice/expressiblebyextendedgraphemeclusterliteral-implementations/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainservice/expressiblebyunicodescalarliteral-implementations/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainservice/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainservice/init(extendedgraphemeclusterliteral:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainservice/init(stringliteral:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainservice/init(unicodescalarliteral:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainservice/init(value:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/keychainservice/value/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/append(_:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/binding/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/init(key:service:group:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/projectedvalue/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/remove()/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/replace(_:with:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/set(_:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/set(_:to:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/toggle()/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/update(key:value:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/values/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/securelystoredvalue/wrappedvalue/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storableitem/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/events/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/init(storage:)-1dbuk/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/init(storage:)-2icz/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/init(storage:)-2zxc4/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/init(storage:)-8ky4y/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/init(storage:cacheidentifier:)-11vez/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/init(storage:cacheidentifier:)-1933a/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/insert(_:)-2vg6j/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/insert(_:)-3j9hw/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/insert(_:)-7z2oe/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/insert(_:)-9n4e3/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/items/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/itemshaveloaded()/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/operation/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/operation/insert(_:)-1nu61/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/operation/insert(_:)-32lwk/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/operation/remove(_:)-2tqlz/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/operation/remove(_:)-8ufsb/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/operation/removeall()/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/operation/run()/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/previewstore(items:)-1azzy/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/previewstore(items:)-1zymp/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/previewstore(items:cacheidentifier:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/remove(_:)-1w3lx/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/remove(_:)-3nzlq/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/remove(_:)-51ya6/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/remove(_:)-5dwyv/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/removeall()-1xc24/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/store/removeall()-9zfmy/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/stored/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/stored/init(in:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/stored/projectedvalue/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/stored/wrappedvalue/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/append(_:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/binding/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/init(key:default:storage:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/init(wrappedvalue:key:storage:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/projectedvalue/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/replace(_:with:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/reset()/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/set(_:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/set(_:to:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/toggle()/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/togglepresence(_:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/update(key:value:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/values/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storedvalue/wrappedvalue/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/init(from:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/items/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/operation-swift.enum/!=(_:_:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/operation-swift.enum/equatable-implementations/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/operation-swift.enum/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/operation-swift.enum/init(from:)/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/operation-swift.enum/initialized/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/operation-swift.enum/insert/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/operation-swift.enum/loaded/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/operation-swift.enum/remove/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/storeevent/operation-swift.property/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/the-@stored-family-of-property-wrappers/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/documentation/boutique/using-stores/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mergesort/Boutique/9379e6a0b13bfb01c2ae655b65962d8479b63428/docs/favicon.ico -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /docs/img/added-icon.832a5d2c.svg: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /docs/img/deprecated-icon.7bf1740a.svg: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /docs/img/modified-icon.efb2697d.svg: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Documentation
-------------------------------------------------------------------------------- /docs/js/highlight-js-diff-js.4db9a783.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * This source file is part of the Swift.org open source project 3 | * 4 | * Copyright (c) 2021 Apple Inc. and the Swift project authors 5 | * Licensed under Apache License v2.0 with Runtime Library Exception 6 | * 7 | * See https://swift.org/LICENSE.txt for license information 8 | * See https://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | (self["webpackChunkswift_docc_render"]=self["webpackChunkswift_docc_render"]||[]).push([[213],{7731:function(e){function n(e){const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{className:"meta",relevance:10,match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/)},{className:"comment",variants:[{begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/),end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/,end:/$/}]}}e.exports=n}}]); -------------------------------------------------------------------------------- /docs/js/highlight-js-http-js.f78e83c2.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * This source file is part of the Swift.org open source project 3 | * 4 | * Copyright (c) 2021 Apple Inc. and the Swift project authors 5 | * Licensed under Apache License v2.0 with Runtime Library Exception 6 | * 7 | * See https://swift.org/LICENSE.txt for license information 8 | * See https://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | (self["webpackChunkswift_docc_render"]=self["webpackChunkswift_docc_render"]||[]).push([[878],{8937:function(e){function n(e){const n=e.regex,a="HTTP/(2|1\\.[01])",s=/[A-Za-z][A-Za-z0-9-]*/,t={className:"attribute",begin:n.concat("^",s,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},i=[t,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+a+" \\d{3})",end:/$/,contains:[{className:"meta",begin:a},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:i}},{begin:"(?=^[A-Z]+ (.*?) "+a+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:a},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:i}},e.inherit(t,{relevance:0})]}}e.exports=n}}]); -------------------------------------------------------------------------------- /docs/js/highlight-js-java-js.4fe21e94.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * This source file is part of the Swift.org open source project 3 | * 4 | * Copyright (c) 2021 Apple Inc. and the Swift project authors 5 | * Licensed under Apache License v2.0 with Runtime Library Exception 6 | * 7 | * See https://swift.org/LICENSE.txt for license information 8 | * See https://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | (self["webpackChunkswift_docc_render"]=self["webpackChunkswift_docc_render"]||[]).push([[788],{8257:function(e){var n="[0-9](_*[0-9])*",a=`\\.(${n})`,s="[0-9a-fA-F](_*[0-9a-fA-F])*",t={className:"number",variants:[{begin:`(\\b(${n})((${a})|\\.)?|(${a}))[eE][+-]?(${n})[fFdD]?\\b`},{begin:`\\b(${n})((${a})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{begin:`(${a})[fFdD]?\\b`},{begin:`\\b(${n})[fFdD]\\b`},{begin:`\\b0[xX]((${s})\\.?|(${s})?\\.(${s}))[pP][+-]?(${n})[fFdD]?\\b`},{begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${s})[lL]?\\b`},{begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}],relevance:0};function i(e,n,a){return-1===a?"":e.replace(n,(s=>i(e,n,a-1)))}function r(e){e.regex;const n="[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",a=n+i("(?:<"+n+"~~~(?:\\s*,\\s*"+n+"~~~)*>)?",/~~~/g,2),s=["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do"],r=["super","this"],c=["false","true","null"],l=["char","boolean","long","float","int","byte","short","double"],b={keyword:s,literal:c,type:l,built_in:r},o={className:"meta",begin:"@"+n,contains:[{begin:/\(/,end:/\)/,contains:["self"]}]},_={className:"params",begin:/\(/,end:/\)/,keywords:b,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0};return{name:"Java",aliases:["jsp"],keywords:b,illegal:/<\/|#/,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{begin:/import java\.[a-z]+\./,keywords:"import",relevance:2},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/,className:"string",contains:[e.BACKSLASH_ESCAPE]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,n],className:{1:"keyword",3:"title.class"}},{begin:[n,/\s+/,n,/\s+/,/=/],className:{1:"type",3:"variable",5:"operator"}},{begin:[/record/,/\s+/,n],className:{1:"keyword",3:"title.class"},contains:[_,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"new throw return else",relevance:0},{begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{2:"title.function"},keywords:b,contains:[{className:"params",begin:/\(/,end:/\)/,keywords:b,relevance:0,contains:[o,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,t,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},t,o]}}e.exports=r}}]); -------------------------------------------------------------------------------- /docs/js/highlight-js-json-js.2a1856ba.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * This source file is part of the Swift.org open source project 3 | * 4 | * Copyright (c) 2021 Apple Inc. and the Swift project authors 5 | * Licensed under Apache License v2.0 with Runtime Library Exception 6 | * 7 | * See https://swift.org/LICENSE.txt for license information 8 | * See https://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | (self["webpackChunkswift_docc_render"]=self["webpackChunkswift_docc_render"]||[]).push([[82],{14:function(e){function n(e){const n={className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},c={match:/[{}[\],:]/,className:"punctuation",relevance:0},a={beginKeywords:["true","false","null"].join(" ")};return{name:"JSON",contains:[n,c,e.QUOTE_STRING_MODE,a,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],illegal:"\\S"}}e.exports=n}}]); -------------------------------------------------------------------------------- /docs/js/highlight-js-markdown-js.a2f456af.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * This source file is part of the Swift.org open source project 3 | * 4 | * Copyright (c) 2021 Apple Inc. and the Swift project authors 5 | * Licensed under Apache License v2.0 with Runtime Library Exception 6 | * 7 | * See https://swift.org/LICENSE.txt for license information 8 | * See https://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | (self["webpackChunkswift_docc_render"]=self["webpackChunkswift_docc_render"]||[]).push([[113],{1312:function(e){function n(e){const n=e.regex,a={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},i={begin:"^[-\\*]{3,}",end:"$"},c={className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},s={className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",end:"\\s+",excludeEnd:!0},t={begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]},d=/[A-Za-z][A-Za-z0-9+.-]*/,l={variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/,relevance:2},{begin:n.concat(/\[.+?\]\(/,d,/:\/\/.*?\)/),relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}]},g={className:"strong",contains:[],variants:[{begin:/_{2}/,end:/_{2}/},{begin:/\*{2}/,end:/\*{2}/}]},b={className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{begin:/_(?!_)/,end:/_/,relevance:0}]};g.contains.push(b),b.contains.push(g);let o=[a,l];g.contains=g.contains.concat(o),b.contains=b.contains.concat(o),o=o.concat(g,b);const r={className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",contains:o}]}]},u={className:"quote",begin:"^>\\s+",contains:o,end:"$"};return{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[r,a,s,g,b,u,c,i,l,t]}}e.exports=n}}]); -------------------------------------------------------------------------------- /docs/js/highlight-js-shell-js.0ad5b20f.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * This source file is part of the Swift.org open source project 3 | * 4 | * Copyright (c) 2021 Apple Inc. and the Swift project authors 5 | * Licensed under Apache License v2.0 with Runtime Library Exception 6 | * 7 | * See https://swift.org/LICENSE.txt for license information 8 | * See https://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | (self["webpackChunkswift_docc_render"]=self["webpackChunkswift_docc_render"]||[]).push([[176],{7874:function(s){function e(s){return{name:"Shell Session",aliases:["console","shellsession"],contains:[{className:"meta",begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/,subLanguage:"bash"}}]}}s.exports=e}}]); -------------------------------------------------------------------------------- /docs/js/highlight-js-xml-js.0d78f903.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * This source file is part of the Swift.org open source project 3 | * 4 | * Copyright (c) 2021 Apple Inc. and the Swift project authors 5 | * Licensed under Apache License v2.0 with Runtime Library Exception 6 | * 7 | * See https://swift.org/LICENSE.txt for license information 8 | * See https://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | (self["webpackChunkswift_docc_render"]=self["webpackChunkswift_docc_render"]||[]).push([[490],{4610:function(e){function n(e){const n=e.regex,a=n.concat(/[A-Z_]/,n.optional(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),s=/[A-Za-z0-9._:-]+/,t={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},c={begin:/\s/,contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},i=e.inherit(c,{begin:/\(/,end:/\)/}),l=e.inherit(e.APOS_STRING_MODE,{className:"string"}),r=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),g={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[c,r,l,i,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[c,i,r,l]}]}]},e.COMMENT(//,{relevance:10}),{begin://,relevance:10},t,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[g],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[g],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:n.concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:a,relevance:0,starts:g}]},{className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(a,/>/))),contains:[{className:"name",begin:a,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}e.exports=n}}]); -------------------------------------------------------------------------------- /docs/metadata.json: -------------------------------------------------------------------------------- 1 | {"schemaVersion":{"minor":1,"major":0,"patch":0},"bundleIdentifier":"Boutique","bundleDisplayName":"Boutique"} -------------------------------------------------------------------------------- /docs/theme-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": {}, 3 | "theme": { 4 | "code": { 5 | "indentationWidth": 4 6 | }, 7 | "colors": { 8 | "text": "", 9 | "text-background": "", 10 | "grid": "", 11 | "article-background": "", 12 | "generic-modal-background": "", 13 | "secondary-label": "", 14 | "header-text": "", 15 | "not-found": { 16 | "input-border": "" 17 | }, 18 | "runtime-preview": { 19 | "text": "" 20 | }, 21 | "tabnav-item": { 22 | "border-color": "" 23 | }, 24 | "svg-icon": { 25 | "fill-light": "", 26 | "fill-dark": "" 27 | }, 28 | "loading-placeholder": { 29 | "background": "" 30 | }, 31 | "button": { 32 | "text": "", 33 | "light": { 34 | "background": "", 35 | "backgroundHover": "", 36 | "backgroundActive": "" 37 | }, 38 | "dark": { 39 | "background": "", 40 | "backgroundHover": "", 41 | "backgroundActive": "" 42 | } 43 | }, 44 | "link": null 45 | }, 46 | "style": { 47 | "button": { 48 | "borderRadius": null 49 | } 50 | }, 51 | "typography": { 52 | "html-font": "" 53 | } 54 | }, 55 | "features": { 56 | "docs": { 57 | } 58 | } 59 | } 60 | --------------------------------------------------------------------------------