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 | 
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]+;|[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:/,relevance:0,contains:[{className:"attr",begin:s,relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[t]},{begin:/'/,end:/'/,contains:[t]},{begin:/[^\s"'=<>`]+/}]}]}]};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:/