├── .github └── workflows │ └── build.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Development ├── DynamicFrameworkComponents │ └── Component.swift ├── StaticLibraryComponents │ └── Component.swift ├── Storybook.xcodeproj │ ├── StorybookKit_Info.plist │ ├── StorybookUI_Info.plist │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ ├── IDETemplateMacros.plist │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ ├── Storybook-Package.xcscheme │ │ └── SwiftUIDemoApp.xcscheme └── SwiftUIDemoApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Component.swift │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── SwiftUIDemoAppApp.swift ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md └── Sources ├── StorybookKit ├── BookGenerator.swift ├── BookLorem.swift ├── Compositions │ ├── BookPadding.swift │ ├── BookPattern.swift │ └── BookSection.swift ├── Internals │ ├── Hosting │ │ ├── _ViewControllerHost.swift │ │ └── _ViewHost.swift │ ├── Preview │ │ └── PreviewRegistryWrapper.swift │ ├── TargetViewControllerKey.swift │ ├── fuzzyMatch.swift │ └── machOLoader.swift ├── Primitives │ ├── Book.swift │ ├── BookAction.swift │ ├── BookPage.swift │ ├── BookPresent.swift │ ├── BookPreview.swift │ ├── BookProvider.swift │ ├── BookPush.swift │ ├── BookStore.swift │ ├── BookText.swift │ └── BookView.swift ├── SearchBar.swift ├── Storybook.swift ├── StorybookDisplayRootView.swift └── StorybookKit.swift ├── StorybookKitTextureSupport ├── BookNodePreview.swift ├── Info.plist └── StorybookKitTextureSupport.h ├── StorybookMacrosPlugin ├── StorybookMacrosPlugin.swift ├── StorybookPageMacro.swift └── StorybookPreviewMacro.swift └── StorybookMacrosTests ├── StorybookPageTests.swift └── StorybookPreviewTests.swift /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-storybook-kit: 7 | runs-on: macos-15 8 | steps: 9 | - uses: maxim-lobanov/setup-xcode@v1.1 10 | with: 11 | xcode-version: "16.2" 12 | - uses: actions/checkout@v2 13 | - name: xcodebuild 14 | run: set -o pipefail && xcodebuild -scheme StorybookKit -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' | xcbeautify 15 | build-storybook-kit-texture-support: 16 | runs-on: macos-15 17 | steps: 18 | - uses: maxim-lobanov/setup-xcode@v1.1 19 | with: 20 | xcode-version: "16.2" 21 | - uses: actions/checkout@v2 22 | - name: xcodebuild 23 | run: set -o pipefail && xcodebuild -scheme StorybookKitTextureSupport -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' | xcbeautify 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .DS_Store 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | .build/ 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | Pods/ 52 | # 53 | # Add this line if you want to avoid checking in source code from the Xcode workspace 54 | # *.xcworkspace 55 | 56 | # Carthage 57 | # 58 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 59 | # Carthage/Checkouts 60 | 61 | Carthage/Build 62 | 63 | # fastlane 64 | # 65 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 66 | # screenshots whenever they are needed. 67 | # For more information about the recommended setup visit: 68 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 69 | 70 | fastlane/report.xml 71 | fastlane/Preview.html 72 | fastlane/screenshots/**/*.png 73 | fastlane/test_output 74 | 75 | # Code Injection 76 | # 77 | # After new code Injection tools there's a generated folder /iOSInjectionProject 78 | # https://github.com/johnno1962/injectionforxcode 79 | 80 | iOSInjectionProject/ 81 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Development/DynamicFrameworkComponents/Component.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | #Preview("Circle") { 25 | Circle() 26 | .fill(.purple) 27 | .frame(width: 100, height: 100) 28 | } 29 | -------------------------------------------------------------------------------- /Development/StaticLibraryComponents/Component.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | #Preview("Circle") { 25 | Circle() 26 | .fill(.purple) 27 | .frame(width: 100, height: 100) 28 | } 29 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/StorybookKit_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/StorybookUI_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 70; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4B8438DA2D4CE6C500BE1DA9 /* libStaticLibraryComponents.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B8438CC2D4CE44C00BE1DA9 /* libStaticLibraryComponents.a */; }; 11 | 4B8438DE2D4CE6E400BE1DA9 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B8438DD2D4CE6E400BE1DA9 /* SwiftUI.framework */; }; 12 | 4B8438DF2D4CE6EC00BE1DA9 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B8438DD2D4CE6E400BE1DA9 /* SwiftUI.framework */; }; 13 | 4BA377E92D4CE2F000D4E565 /* DynamicFrameworkComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BA377D62D4CE2F000D4E565 /* DynamicFrameworkComponents.framework */; }; 14 | 4BA377EA2D4CE2F000D4E565 /* DynamicFrameworkComponents.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4BA377D62D4CE2F000D4E565 /* DynamicFrameworkComponents.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15 | 4BA377F42D4CE31D00D4E565 /* StorybookKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4BA377F32D4CE31D00D4E565 /* StorybookKit */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXContainerItemProxy section */ 19 | 4B8438DB2D4CE6C500BE1DA9 /* PBXContainerItemProxy */ = { 20 | isa = PBXContainerItemProxy; 21 | containerPortal = 957A89282226610200CDD25D /* Project object */; 22 | proxyType = 1; 23 | remoteGlobalIDString = 4B8438CB2D4CE44C00BE1DA9; 24 | remoteInfo = StaticLibraryComponents; 25 | }; 26 | 4BA377E72D4CE2F000D4E565 /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = 957A89282226610200CDD25D /* Project object */; 29 | proxyType = 1; 30 | remoteGlobalIDString = 4BA377D52D4CE2F000D4E565; 31 | remoteInfo = DynamicFrameworkComponents; 32 | }; 33 | /* End PBXContainerItemProxy section */ 34 | 35 | /* Begin PBXCopyFilesBuildPhase section */ 36 | 4B8438CA2D4CE44C00BE1DA9 /* CopyFiles */ = { 37 | isa = PBXCopyFilesBuildPhase; 38 | buildActionMask = 2147483647; 39 | dstPath = "include/$(PRODUCT_NAME)"; 40 | dstSubfolderSpec = 16; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | 4BA377EF2D4CE2F000D4E565 /* Embed Frameworks */ = { 46 | isa = PBXCopyFilesBuildPhase; 47 | buildActionMask = 2147483647; 48 | dstPath = ""; 49 | dstSubfolderSpec = 10; 50 | files = ( 51 | 4BA377EA2D4CE2F000D4E565 /* DynamicFrameworkComponents.framework in Embed Frameworks */, 52 | ); 53 | name = "Embed Frameworks"; 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | /* End PBXCopyFilesBuildPhase section */ 57 | 58 | /* Begin PBXFileReference section */ 59 | 4B0BDD7C2A87A4A200CF2633 /* Storybook-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "Storybook-ios"; path = ..; sourceTree = ""; }; 60 | 4B8438CC2D4CE44C00BE1DA9 /* libStaticLibraryComponents.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libStaticLibraryComponents.a; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 4B8438DD2D4CE6E400BE1DA9 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.2.sdk/System/iOSSupport/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; }; 62 | 4BA377A72D4CE2C500D4E565 /* SwiftUIDemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUIDemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | 4BA377D62D4CE2F000D4E565 /* DynamicFrameworkComponents.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DynamicFrameworkComponents.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | /* End PBXFileReference section */ 65 | 66 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 67 | 4B8438CD2D4CE44C00BE1DA9 /* StaticLibraryComponents */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = StaticLibraryComponents; sourceTree = ""; }; 68 | 4BA377A82D4CE2C500D4E565 /* SwiftUIDemoApp */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SwiftUIDemoApp; sourceTree = ""; }; 69 | 4BA377D72D4CE2F000D4E565 /* DynamicFrameworkComponents */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = DynamicFrameworkComponents; sourceTree = ""; }; 70 | /* End PBXFileSystemSynchronizedRootGroup section */ 71 | 72 | /* Begin PBXFrameworksBuildPhase section */ 73 | 4B8438C92D4CE44C00BE1DA9 /* Frameworks */ = { 74 | isa = PBXFrameworksBuildPhase; 75 | buildActionMask = 2147483647; 76 | files = ( 77 | 4B8438DF2D4CE6EC00BE1DA9 /* SwiftUI.framework in Frameworks */, 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | 4BA377A42D4CE2C500D4E565 /* Frameworks */ = { 82 | isa = PBXFrameworksBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | 4BA377F42D4CE31D00D4E565 /* StorybookKit in Frameworks */, 86 | 4BA377E92D4CE2F000D4E565 /* DynamicFrameworkComponents.framework in Frameworks */, 87 | 4B8438DA2D4CE6C500BE1DA9 /* libStaticLibraryComponents.a in Frameworks */, 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | 4BA377D32D4CE2F000D4E565 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | 4B8438DE2D4CE6E400BE1DA9 /* SwiftUI.framework in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | /* End PBXFrameworksBuildPhase section */ 100 | 101 | /* Begin PBXGroup section */ 102 | 4B0BDD7D2A87A4C800CF2633 /* Frameworks */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 4B8438DD2D4CE6E400BE1DA9 /* SwiftUI.framework */, 106 | ); 107 | name = Frameworks; 108 | sourceTree = ""; 109 | }; 110 | 957A89272226610200CDD25D = { 111 | isa = PBXGroup; 112 | children = ( 113 | 4B0BDD7C2A87A4A200CF2633 /* Storybook-ios */, 114 | 4BA377A82D4CE2C500D4E565 /* SwiftUIDemoApp */, 115 | 4BA377D72D4CE2F000D4E565 /* DynamicFrameworkComponents */, 116 | 4B8438CD2D4CE44C00BE1DA9 /* StaticLibraryComponents */, 117 | 957A89322226610200CDD25D /* Products */, 118 | 4B0BDD7D2A87A4C800CF2633 /* Frameworks */, 119 | ); 120 | indentWidth = 2; 121 | sourceTree = ""; 122 | tabWidth = 2; 123 | }; 124 | 957A89322226610200CDD25D /* Products */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 4BA377A72D4CE2C500D4E565 /* SwiftUIDemoApp.app */, 128 | 4BA377D62D4CE2F000D4E565 /* DynamicFrameworkComponents.framework */, 129 | 4B8438CC2D4CE44C00BE1DA9 /* libStaticLibraryComponents.a */, 130 | ); 131 | name = Products; 132 | sourceTree = ""; 133 | }; 134 | /* End PBXGroup section */ 135 | 136 | /* Begin PBXHeadersBuildPhase section */ 137 | 4BA377D12D4CE2F000D4E565 /* Headers */ = { 138 | isa = PBXHeadersBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | /* End PBXHeadersBuildPhase section */ 145 | 146 | /* Begin PBXNativeTarget section */ 147 | 4B8438CB2D4CE44C00BE1DA9 /* StaticLibraryComponents */ = { 148 | isa = PBXNativeTarget; 149 | buildConfigurationList = 4B8438D02D4CE44C00BE1DA9 /* Build configuration list for PBXNativeTarget "StaticLibraryComponents" */; 150 | buildPhases = ( 151 | 4B8438C82D4CE44C00BE1DA9 /* Sources */, 152 | 4B8438C92D4CE44C00BE1DA9 /* Frameworks */, 153 | 4B8438CA2D4CE44C00BE1DA9 /* CopyFiles */, 154 | ); 155 | buildRules = ( 156 | ); 157 | dependencies = ( 158 | ); 159 | fileSystemSynchronizedGroups = ( 160 | 4B8438CD2D4CE44C00BE1DA9 /* StaticLibraryComponents */, 161 | ); 162 | name = StaticLibraryComponents; 163 | packageProductDependencies = ( 164 | ); 165 | productName = StaticLibraryComponents; 166 | productReference = 4B8438CC2D4CE44C00BE1DA9 /* libStaticLibraryComponents.a */; 167 | productType = "com.apple.product-type.library.static"; 168 | }; 169 | 4BA377A62D4CE2C500D4E565 /* SwiftUIDemoApp */ = { 170 | isa = PBXNativeTarget; 171 | buildConfigurationList = 4BA377CE2D4CE2C700D4E565 /* Build configuration list for PBXNativeTarget "SwiftUIDemoApp" */; 172 | buildPhases = ( 173 | 4BA377A32D4CE2C500D4E565 /* Sources */, 174 | 4BA377A42D4CE2C500D4E565 /* Frameworks */, 175 | 4BA377A52D4CE2C500D4E565 /* Resources */, 176 | 4BA377EF2D4CE2F000D4E565 /* Embed Frameworks */, 177 | ); 178 | buildRules = ( 179 | ); 180 | dependencies = ( 181 | 4BA377E82D4CE2F000D4E565 /* PBXTargetDependency */, 182 | 4B8438DC2D4CE6C500BE1DA9 /* PBXTargetDependency */, 183 | ); 184 | fileSystemSynchronizedGroups = ( 185 | 4BA377A82D4CE2C500D4E565 /* SwiftUIDemoApp */, 186 | ); 187 | name = SwiftUIDemoApp; 188 | packageProductDependencies = ( 189 | 4BA377F32D4CE31D00D4E565 /* StorybookKit */, 190 | ); 191 | productName = SwiftUIDemoApp; 192 | productReference = 4BA377A72D4CE2C500D4E565 /* SwiftUIDemoApp.app */; 193 | productType = "com.apple.product-type.application"; 194 | }; 195 | 4BA377D52D4CE2F000D4E565 /* DynamicFrameworkComponents */ = { 196 | isa = PBXNativeTarget; 197 | buildConfigurationList = 4BA377EC2D4CE2F000D4E565 /* Build configuration list for PBXNativeTarget "DynamicFrameworkComponents" */; 198 | buildPhases = ( 199 | 4BA377D12D4CE2F000D4E565 /* Headers */, 200 | 4BA377D22D4CE2F000D4E565 /* Sources */, 201 | 4BA377D32D4CE2F000D4E565 /* Frameworks */, 202 | 4BA377D42D4CE2F000D4E565 /* Resources */, 203 | ); 204 | buildRules = ( 205 | ); 206 | dependencies = ( 207 | ); 208 | fileSystemSynchronizedGroups = ( 209 | 4BA377D72D4CE2F000D4E565 /* DynamicFrameworkComponents */, 210 | ); 211 | name = DynamicFrameworkComponents; 212 | packageProductDependencies = ( 213 | ); 214 | productName = DynamicFrameworkComponents; 215 | productReference = 4BA377D62D4CE2F000D4E565 /* DynamicFrameworkComponents.framework */; 216 | productType = "com.apple.product-type.framework"; 217 | }; 218 | /* End PBXNativeTarget section */ 219 | 220 | /* Begin PBXProject section */ 221 | 957A89282226610200CDD25D /* Project object */ = { 222 | isa = PBXProject; 223 | attributes = { 224 | LastSwiftUpdateCheck = 1620; 225 | LastUpgradeCheck = 1010; 226 | ORGANIZATIONNAME = "eureka, Inc."; 227 | TargetAttributes = { 228 | 4B8438CB2D4CE44C00BE1DA9 = { 229 | CreatedOnToolsVersion = 16.2; 230 | }; 231 | 4BA377A62D4CE2C500D4E565 = { 232 | CreatedOnToolsVersion = 16.2; 233 | }; 234 | 4BA377D52D4CE2F000D4E565 = { 235 | CreatedOnToolsVersion = 16.2; 236 | LastSwiftMigration = 1620; 237 | }; 238 | }; 239 | }; 240 | buildConfigurationList = 957A892B2226610200CDD25D /* Build configuration list for PBXProject "Storybook" */; 241 | compatibilityVersion = "Xcode 9.3"; 242 | developmentRegion = en; 243 | hasScannedForEncodings = 0; 244 | knownRegions = ( 245 | en, 246 | Base, 247 | ); 248 | mainGroup = 957A89272226610200CDD25D; 249 | packageReferences = ( 250 | 4BB655B82A87B0C200F2E2D7 /* XCRemoteSwiftPackageReference "swiftui-support" */, 251 | ); 252 | productRefGroup = 957A89322226610200CDD25D /* Products */; 253 | projectDirPath = ""; 254 | projectRoot = ""; 255 | targets = ( 256 | 4BA377A62D4CE2C500D4E565 /* SwiftUIDemoApp */, 257 | 4BA377D52D4CE2F000D4E565 /* DynamicFrameworkComponents */, 258 | 4B8438CB2D4CE44C00BE1DA9 /* StaticLibraryComponents */, 259 | ); 260 | }; 261 | /* End PBXProject section */ 262 | 263 | /* Begin PBXResourcesBuildPhase section */ 264 | 4BA377A52D4CE2C500D4E565 /* Resources */ = { 265 | isa = PBXResourcesBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | ); 269 | runOnlyForDeploymentPostprocessing = 0; 270 | }; 271 | 4BA377D42D4CE2F000D4E565 /* Resources */ = { 272 | isa = PBXResourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | /* End PBXResourcesBuildPhase section */ 279 | 280 | /* Begin PBXSourcesBuildPhase section */ 281 | 4B8438C82D4CE44C00BE1DA9 /* Sources */ = { 282 | isa = PBXSourcesBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | ); 286 | runOnlyForDeploymentPostprocessing = 0; 287 | }; 288 | 4BA377A32D4CE2C500D4E565 /* Sources */ = { 289 | isa = PBXSourcesBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | 4BA377D22D4CE2F000D4E565 /* Sources */ = { 296 | isa = PBXSourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | /* End PBXSourcesBuildPhase section */ 303 | 304 | /* Begin PBXTargetDependency section */ 305 | 4B8438DC2D4CE6C500BE1DA9 /* PBXTargetDependency */ = { 306 | isa = PBXTargetDependency; 307 | target = 4B8438CB2D4CE44C00BE1DA9 /* StaticLibraryComponents */; 308 | targetProxy = 4B8438DB2D4CE6C500BE1DA9 /* PBXContainerItemProxy */; 309 | }; 310 | 4BA377E82D4CE2F000D4E565 /* PBXTargetDependency */ = { 311 | isa = PBXTargetDependency; 312 | target = 4BA377D52D4CE2F000D4E565 /* DynamicFrameworkComponents */; 313 | targetProxy = 4BA377E72D4CE2F000D4E565 /* PBXContainerItemProxy */; 314 | }; 315 | /* End PBXTargetDependency section */ 316 | 317 | /* Begin XCBuildConfiguration section */ 318 | 4B8438D12D4CE44C00BE1DA9 /* Debug */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 322 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 323 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 324 | CODE_SIGN_STYLE = Automatic; 325 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 326 | GCC_C_LANGUAGE_STANDARD = gnu17; 327 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 328 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 329 | OTHER_LDFLAGS = "-ObjC"; 330 | PRODUCT_NAME = "$(TARGET_NAME)"; 331 | SKIP_INSTALL = YES; 332 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 333 | SWIFT_VERSION = 5.0; 334 | TARGETED_DEVICE_FAMILY = "1,2"; 335 | }; 336 | name = Debug; 337 | }; 338 | 4B8438D22D4CE44C00BE1DA9 /* Release */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 342 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 343 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 344 | CODE_SIGN_STYLE = Automatic; 345 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 346 | GCC_C_LANGUAGE_STANDARD = gnu17; 347 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 348 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 349 | OTHER_LDFLAGS = "-ObjC"; 350 | PRODUCT_NAME = "$(TARGET_NAME)"; 351 | SKIP_INSTALL = YES; 352 | SWIFT_VERSION = 5.0; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | }; 355 | name = Release; 356 | }; 357 | 4BA377C82D4CE2C700D4E565 /* Debug */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 361 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 362 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 363 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 364 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 365 | CODE_SIGN_STYLE = Automatic; 366 | CURRENT_PROJECT_VERSION = 1; 367 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUIDemoApp/Preview Content\""; 368 | ENABLE_PREVIEWS = YES; 369 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 370 | GCC_C_LANGUAGE_STANDARD = gnu17; 371 | GENERATE_INFOPLIST_FILE = YES; 372 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 373 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 374 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 375 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 376 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 377 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 378 | LD_RUNPATH_SEARCH_PATHS = ( 379 | "$(inherited)", 380 | "@executable_path/Frameworks", 381 | ); 382 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 383 | MARKETING_VERSION = 1.0; 384 | OTHER_LDFLAGS = "-all_load"; 385 | PRODUCT_BUNDLE_IDENTIFIER = jp.eure.SwiftUIDemoApp; 386 | PRODUCT_NAME = "$(TARGET_NAME)"; 387 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 388 | SWIFT_EMIT_LOC_STRINGS = YES; 389 | SWIFT_VERSION = 5.0; 390 | TARGETED_DEVICE_FAMILY = "1,2"; 391 | }; 392 | name = Debug; 393 | }; 394 | 4BA377C92D4CE2C700D4E565 /* Release */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 398 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 399 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 400 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 401 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 402 | CODE_SIGN_STYLE = Automatic; 403 | CURRENT_PROJECT_VERSION = 1; 404 | DEVELOPMENT_ASSET_PATHS = "\"SwiftUIDemoApp/Preview Content\""; 405 | ENABLE_PREVIEWS = YES; 406 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 407 | GCC_C_LANGUAGE_STANDARD = gnu17; 408 | GENERATE_INFOPLIST_FILE = YES; 409 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 410 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 411 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 412 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 413 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 414 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 415 | LD_RUNPATH_SEARCH_PATHS = ( 416 | "$(inherited)", 417 | "@executable_path/Frameworks", 418 | ); 419 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 420 | MARKETING_VERSION = 1.0; 421 | OTHER_LDFLAGS = "-all_load"; 422 | PRODUCT_BUNDLE_IDENTIFIER = jp.eure.SwiftUIDemoApp; 423 | PRODUCT_NAME = "$(TARGET_NAME)"; 424 | SWIFT_EMIT_LOC_STRINGS = YES; 425 | SWIFT_VERSION = 5.0; 426 | TARGETED_DEVICE_FAMILY = "1,2"; 427 | }; 428 | name = Release; 429 | }; 430 | 4BA377ED2D4CE2F000D4E565 /* Debug */ = { 431 | isa = XCBuildConfiguration; 432 | buildSettings = { 433 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 434 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 435 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 436 | CLANG_ENABLE_MODULES = YES; 437 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 438 | CODE_SIGN_STYLE = Automatic; 439 | CURRENT_PROJECT_VERSION = 1; 440 | DEFINES_MODULE = YES; 441 | DYLIB_COMPATIBILITY_VERSION = 1; 442 | DYLIB_CURRENT_VERSION = 1; 443 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 444 | ENABLE_MODULE_VERIFIER = YES; 445 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 446 | GCC_C_LANGUAGE_STANDARD = gnu17; 447 | GENERATE_INFOPLIST_FILE = YES; 448 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 eureka, Inc. All rights reserved."; 449 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 450 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 451 | LD_RUNPATH_SEARCH_PATHS = ( 452 | "$(inherited)", 453 | "@executable_path/Frameworks", 454 | "@loader_path/Frameworks", 455 | ); 456 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 457 | MARKETING_VERSION = 1.0; 458 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 459 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 460 | PRODUCT_BUNDLE_IDENTIFIER = jp.eure.DynamicFrameworkComponents; 461 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 462 | SKIP_INSTALL = YES; 463 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 464 | SWIFT_EMIT_LOC_STRINGS = YES; 465 | SWIFT_INSTALL_OBJC_HEADER = NO; 466 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 467 | SWIFT_VERSION = 5.0; 468 | TARGETED_DEVICE_FAMILY = "1,2"; 469 | }; 470 | name = Debug; 471 | }; 472 | 4BA377EE2D4CE2F000D4E565 /* Release */ = { 473 | isa = XCBuildConfiguration; 474 | buildSettings = { 475 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 476 | BUILD_LIBRARY_FOR_DISTRIBUTION = YES; 477 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 478 | CLANG_ENABLE_MODULES = YES; 479 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 480 | CODE_SIGN_STYLE = Automatic; 481 | CURRENT_PROJECT_VERSION = 1; 482 | DEFINES_MODULE = YES; 483 | DYLIB_COMPATIBILITY_VERSION = 1; 484 | DYLIB_CURRENT_VERSION = 1; 485 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 486 | ENABLE_MODULE_VERIFIER = YES; 487 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 488 | GCC_C_LANGUAGE_STANDARD = gnu17; 489 | GENERATE_INFOPLIST_FILE = YES; 490 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 eureka, Inc. All rights reserved."; 491 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 492 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 493 | LD_RUNPATH_SEARCH_PATHS = ( 494 | "$(inherited)", 495 | "@executable_path/Frameworks", 496 | "@loader_path/Frameworks", 497 | ); 498 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 499 | MARKETING_VERSION = 1.0; 500 | MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; 501 | MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; 502 | PRODUCT_BUNDLE_IDENTIFIER = jp.eure.DynamicFrameworkComponents; 503 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 504 | SKIP_INSTALL = YES; 505 | SWIFT_EMIT_LOC_STRINGS = YES; 506 | SWIFT_INSTALL_OBJC_HEADER = NO; 507 | SWIFT_VERSION = 5.0; 508 | TARGETED_DEVICE_FAMILY = "1,2"; 509 | }; 510 | name = Release; 511 | }; 512 | 957A89372226610200CDD25D /* Debug */ = { 513 | isa = XCBuildConfiguration; 514 | buildSettings = { 515 | ALWAYS_SEARCH_USER_PATHS = NO; 516 | CLANG_ANALYZER_NONNULL = YES; 517 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 518 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 519 | CLANG_CXX_LIBRARY = "libc++"; 520 | CLANG_ENABLE_MODULES = YES; 521 | CLANG_ENABLE_OBJC_ARC = YES; 522 | CLANG_ENABLE_OBJC_WEAK = YES; 523 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 524 | CLANG_WARN_BOOL_CONVERSION = YES; 525 | CLANG_WARN_COMMA = YES; 526 | CLANG_WARN_CONSTANT_CONVERSION = YES; 527 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 528 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 529 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 530 | CLANG_WARN_EMPTY_BODY = YES; 531 | CLANG_WARN_ENUM_CONVERSION = YES; 532 | CLANG_WARN_INFINITE_RECURSION = YES; 533 | CLANG_WARN_INT_CONVERSION = YES; 534 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 535 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 536 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 537 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 538 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 539 | CLANG_WARN_STRICT_PROTOTYPES = YES; 540 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 541 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 542 | CLANG_WARN_UNREACHABLE_CODE = YES; 543 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 544 | CODE_SIGN_IDENTITY = "iPhone Developer"; 545 | COPY_PHASE_STRIP = NO; 546 | CURRENT_PROJECT_VERSION = 1; 547 | DEBUG_INFORMATION_FORMAT = dwarf; 548 | ENABLE_STRICT_OBJC_MSGSEND = YES; 549 | ENABLE_TESTABILITY = YES; 550 | GCC_C_LANGUAGE_STANDARD = gnu11; 551 | GCC_DYNAMIC_NO_PIC = NO; 552 | GCC_NO_COMMON_BLOCKS = YES; 553 | GCC_OPTIMIZATION_LEVEL = 0; 554 | GCC_PREPROCESSOR_DEFINITIONS = ( 555 | "DEBUG=1", 556 | "$(inherited)", 557 | ); 558 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 559 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 560 | GCC_WARN_UNDECLARED_SELECTOR = YES; 561 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 562 | GCC_WARN_UNUSED_FUNCTION = YES; 563 | GCC_WARN_UNUSED_VARIABLE = YES; 564 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 565 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 566 | MTL_FAST_MATH = YES; 567 | ONLY_ACTIVE_ARCH = YES; 568 | SDKROOT = iphoneos; 569 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 570 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 571 | SWIFT_STRICT_CONCURRENCY = complete; 572 | VERSIONING_SYSTEM = "apple-generic"; 573 | VERSION_INFO_PREFIX = ""; 574 | }; 575 | name = Debug; 576 | }; 577 | 957A89382226610200CDD25D /* Release */ = { 578 | isa = XCBuildConfiguration; 579 | buildSettings = { 580 | ALWAYS_SEARCH_USER_PATHS = NO; 581 | CLANG_ANALYZER_NONNULL = YES; 582 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 583 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 584 | CLANG_CXX_LIBRARY = "libc++"; 585 | CLANG_ENABLE_MODULES = YES; 586 | CLANG_ENABLE_OBJC_ARC = YES; 587 | CLANG_ENABLE_OBJC_WEAK = YES; 588 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 589 | CLANG_WARN_BOOL_CONVERSION = YES; 590 | CLANG_WARN_COMMA = YES; 591 | CLANG_WARN_CONSTANT_CONVERSION = YES; 592 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 593 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 594 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 595 | CLANG_WARN_EMPTY_BODY = YES; 596 | CLANG_WARN_ENUM_CONVERSION = YES; 597 | CLANG_WARN_INFINITE_RECURSION = YES; 598 | CLANG_WARN_INT_CONVERSION = YES; 599 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 600 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 601 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 602 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 603 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 604 | CLANG_WARN_STRICT_PROTOTYPES = YES; 605 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 606 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 607 | CLANG_WARN_UNREACHABLE_CODE = YES; 608 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 609 | CODE_SIGN_IDENTITY = "iPhone Developer"; 610 | COPY_PHASE_STRIP = NO; 611 | CURRENT_PROJECT_VERSION = 1; 612 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 613 | ENABLE_NS_ASSERTIONS = NO; 614 | ENABLE_STRICT_OBJC_MSGSEND = YES; 615 | GCC_C_LANGUAGE_STANDARD = gnu11; 616 | GCC_NO_COMMON_BLOCKS = YES; 617 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 618 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 619 | GCC_WARN_UNDECLARED_SELECTOR = YES; 620 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 621 | GCC_WARN_UNUSED_FUNCTION = YES; 622 | GCC_WARN_UNUSED_VARIABLE = YES; 623 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 624 | MTL_ENABLE_DEBUG_INFO = NO; 625 | MTL_FAST_MATH = YES; 626 | SDKROOT = iphoneos; 627 | SWIFT_COMPILATION_MODE = wholemodule; 628 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 629 | SWIFT_STRICT_CONCURRENCY = complete; 630 | VALIDATE_PRODUCT = YES; 631 | VERSIONING_SYSTEM = "apple-generic"; 632 | VERSION_INFO_PREFIX = ""; 633 | }; 634 | name = Release; 635 | }; 636 | /* End XCBuildConfiguration section */ 637 | 638 | /* Begin XCConfigurationList section */ 639 | 4B8438D02D4CE44C00BE1DA9 /* Build configuration list for PBXNativeTarget "StaticLibraryComponents" */ = { 640 | isa = XCConfigurationList; 641 | buildConfigurations = ( 642 | 4B8438D12D4CE44C00BE1DA9 /* Debug */, 643 | 4B8438D22D4CE44C00BE1DA9 /* Release */, 644 | ); 645 | defaultConfigurationIsVisible = 0; 646 | defaultConfigurationName = Release; 647 | }; 648 | 4BA377CE2D4CE2C700D4E565 /* Build configuration list for PBXNativeTarget "SwiftUIDemoApp" */ = { 649 | isa = XCConfigurationList; 650 | buildConfigurations = ( 651 | 4BA377C82D4CE2C700D4E565 /* Debug */, 652 | 4BA377C92D4CE2C700D4E565 /* Release */, 653 | ); 654 | defaultConfigurationIsVisible = 0; 655 | defaultConfigurationName = Release; 656 | }; 657 | 4BA377EC2D4CE2F000D4E565 /* Build configuration list for PBXNativeTarget "DynamicFrameworkComponents" */ = { 658 | isa = XCConfigurationList; 659 | buildConfigurations = ( 660 | 4BA377ED2D4CE2F000D4E565 /* Debug */, 661 | 4BA377EE2D4CE2F000D4E565 /* Release */, 662 | ); 663 | defaultConfigurationIsVisible = 0; 664 | defaultConfigurationName = Release; 665 | }; 666 | 957A892B2226610200CDD25D /* Build configuration list for PBXProject "Storybook" */ = { 667 | isa = XCConfigurationList; 668 | buildConfigurations = ( 669 | 957A89372226610200CDD25D /* Debug */, 670 | 957A89382226610200CDD25D /* Release */, 671 | ); 672 | defaultConfigurationIsVisible = 0; 673 | defaultConfigurationName = Release; 674 | }; 675 | /* End XCConfigurationList section */ 676 | 677 | /* Begin XCRemoteSwiftPackageReference section */ 678 | 4BB655B82A87B0C200F2E2D7 /* XCRemoteSwiftPackageReference "swiftui-support" */ = { 679 | isa = XCRemoteSwiftPackageReference; 680 | repositoryURL = "https://github.com/FluidGroup/swiftui-support/"; 681 | requirement = { 682 | kind = upToNextMajorVersion; 683 | minimumVersion = 0.4.1; 684 | }; 685 | }; 686 | /* End XCRemoteSwiftPackageReference section */ 687 | 688 | /* Begin XCSwiftPackageProductDependency section */ 689 | 4BA377F32D4CE31D00D4E565 /* StorybookKit */ = { 690 | isa = XCSwiftPackageProductDependency; 691 | productName = StorybookKit; 692 | }; 693 | /* End XCSwiftPackageProductDependency section */ 694 | }; 695 | rootObject = 957A89282226610200CDD25D /* Project object */; 696 | } 697 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "3a6dfeefaa06e81f068454f6d3b99782d6b5b64dffd02869d95d3ca2528f5116", 3 | "pins" : [ 4 | { 5 | "identity" : "descriptors", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/FluidGroup/Descriptors", 8 | "state" : { 9 | "revision" : "f41ce2605a76c5d378fe8c5e8c5c98b544dfd108", 10 | "version" : "0.2.3" 11 | } 12 | }, 13 | { 14 | "identity" : "resultbuilderkit", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/FluidGroup/ResultBuilderKit", 17 | "state" : { 18 | "revision" : "34aa57fcee5ec3ee0f368b05cb53c7919c188ab2", 19 | "version" : "1.3.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-custom-dump", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 26 | "state" : { 27 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", 28 | "version" : "1.3.3" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-macro-testing", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/pointfreeco/swift-macro-testing.git", 35 | "state" : { 36 | "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", 37 | "version" : "0.5.2" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-snapshot-testing", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 44 | "state" : { 45 | "revision" : "f5bfff796ee8e3bc9a685b7ffba1bf20663eb370", 46 | "version" : "1.18.0" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-syntax", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/apple/swift-syntax.git", 53 | "state" : { 54 | "revision" : "0687f71944021d616d34d922343dcef086855920", 55 | "version" : "600.0.1" 56 | } 57 | }, 58 | { 59 | "identity" : "swiftui-gesture-velocity", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/FluidGroup/swiftui-gesture-velocity", 62 | "state" : { 63 | "revision" : "9c83f8995f9e5efc29db2fca4b9ff058283f1603", 64 | "version" : "1.0.0" 65 | } 66 | }, 67 | { 68 | "identity" : "swiftui-support", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/FluidGroup/swiftui-support", 71 | "state" : { 72 | "revision" : "8ef53190c33bd345e7a95ef504dafe0f85ad9c4d", 73 | "version" : "0.4.1" 74 | } 75 | }, 76 | { 77 | "identity" : "texture", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/FluidGroup/Texture.git", 80 | "state" : { 81 | "revision" : "68df47f0d26522da76b06f22e9a97e4d4ab58dad", 82 | "version" : "3.0.2" 83 | } 84 | }, 85 | { 86 | "identity" : "texturebridging", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/FluidGroup/TextureBridging.git", 89 | "state" : { 90 | "revision" : "4383f8a9846a0507d2c22ee9fac22153f9b86fed", 91 | "version" : "3.2.1" 92 | } 93 | }, 94 | { 95 | "identity" : "textureswiftsupport", 96 | "kind" : "remoteSourceControl", 97 | "location" : "https://github.com/FluidGroup/TextureSwiftSupport.git", 98 | "state" : { 99 | "revision" : "33a0adcb38b92debf6b57eda98df06b07055dd73", 100 | "version" : "3.23.0" 101 | } 102 | }, 103 | { 104 | "identity" : "xctest-dynamic-overlay", 105 | "kind" : "remoteSourceControl", 106 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 107 | "state" : { 108 | "revision" : "b444594f79844b0d6d76d70fbfb3f7f71728f938", 109 | "version" : "1.5.1" 110 | } 111 | } 112 | ], 113 | "version" : 3 114 | } 115 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // Copyright (c) 2020 Eureka, Inc. 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | 28 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 16 | 17 | 19 | 21 | 22 | 23 | 24 | 25 | 33 | 34 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/xcshareddata/xcschemes/Storybook-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 57 | 58 | 64 | 65 | 67 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Development/Storybook.xcodeproj/xcshareddata/xcschemes/SwiftUIDemoApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Development/SwiftUIDemoApp/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 | -------------------------------------------------------------------------------- /Development/SwiftUIDemoApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Development/SwiftUIDemoApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Development/SwiftUIDemoApp/Component.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | #Preview("Circle") { 25 | Circle() 26 | .fill(.purple) 27 | .frame(width: 100, height: 100) 28 | } 29 | 30 | #Preview("Circle2") { 31 | Circle() 32 | .fill(.purple) 33 | .frame(width: 100, height: 100) 34 | } 35 | 36 | #Preview { 37 | Circle() 38 | .fill(.purple) 39 | .frame(width: 100, height: 100) 40 | } 41 | -------------------------------------------------------------------------------- /Development/SwiftUIDemoApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import StorybookKit 23 | import SwiftUI 24 | 25 | struct ContentView: View { 26 | var body: some View { 27 | Storybook() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Development/SwiftUIDemoApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Development/SwiftUIDemoApp/SwiftUIDemoAppApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | @main 25 | struct SwiftUIDemoAppApp: App { 26 | var body: some Scene { 27 | WindowGroup { 28 | ContentView() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Eureka, inc. 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | trunk: 3 | pod trunk push --allow-warnings StorybookKit.podspec 4 | pod trunk push --allow-warnings StorybookUI.podspec 5 | pod trunk push --allow-warnings StorybookKitTextureSupport.podspec 6 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "4eb04f1e4297cdd13b29cb7f5cf1d72921892414696f00aceda4577636790300", 3 | "pins" : [ 4 | { 5 | "identity" : "descriptors", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/FluidGroup/Descriptors", 8 | "state" : { 9 | "revision" : "f41ce2605a76c5d378fe8c5e8c5c98b544dfd108", 10 | "version" : "0.2.3" 11 | } 12 | }, 13 | { 14 | "identity" : "resultbuilderkit", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/FluidGroup/ResultBuilderKit", 17 | "state" : { 18 | "revision" : "34aa57fcee5ec3ee0f368b05cb53c7919c188ab2", 19 | "version" : "1.3.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-macro-testing", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/pointfreeco/swift-macro-testing.git", 26 | "state" : { 27 | "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", 28 | "version" : "0.5.2" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-snapshot-testing", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 35 | "state" : { 36 | "revision" : "7b0bbbae90c41f848f90ac7b4df6c4f50068256d", 37 | "version" : "1.17.5" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-syntax", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-syntax.git", 44 | "state" : { 45 | "revision" : "0687f71944021d616d34d922343dcef086855920", 46 | "version" : "600.0.1" 47 | } 48 | }, 49 | { 50 | "identity" : "swiftui-support", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/FluidGroup/swiftui-support", 53 | "state" : { 54 | "revision" : "266e052494f8b7432374ad19a4bd31877cf54640", 55 | "version" : "0.10.0" 56 | } 57 | }, 58 | { 59 | "identity" : "texture", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/FluidGroup/Texture.git", 62 | "state" : { 63 | "revision" : "68df47f0d26522da76b06f22e9a97e4d4ab58dad", 64 | "version" : "3.0.2" 65 | } 66 | }, 67 | { 68 | "identity" : "texturebridging", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/FluidGroup/TextureBridging.git", 71 | "state" : { 72 | "revision" : "4383f8a9846a0507d2c22ee9fac22153f9b86fed", 73 | "version" : "3.2.1" 74 | } 75 | }, 76 | { 77 | "identity" : "textureswiftsupport", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/FluidGroup/TextureSwiftSupport.git", 80 | "state" : { 81 | "revision" : "fb748d6a9d0a2dca0635227e1db0360fd26e0e24", 82 | "version" : "3.20.1" 83 | } 84 | } 85 | ], 86 | "version" : 3 87 | } 88 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | import CompilerPluginSupport 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Storybook", 7 | platforms: [ 8 | .iOS(.v16), 9 | .macCatalyst(.v15), 10 | .macOS(.v10_15), 11 | ], 12 | products: [ 13 | .library(name: "StorybookKit", targets: ["StorybookKit"]), 14 | .library(name: "StorybookKitTextureSupport", targets: ["StorybookKitTextureSupport"]), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/FluidGroup/TextureBridging.git", from: "3.2.1"), 18 | .package(url: "https://github.com/FluidGroup/TextureSwiftSupport.git", from: "3.20.1"), 19 | .package(url: "https://github.com/FluidGroup/swiftui-support", from: "0.4.1"), 20 | .package(url: "https://github.com/FluidGroup/ResultBuilderKit", from: "1.3.0"), 21 | .package(url: "https://github.com/apple/swift-syntax.git", from: "600.0.1"), 22 | .package(url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.5.2"), 23 | ], 24 | targets: [ 25 | .target( 26 | name: "StorybookKit", 27 | dependencies: [ 28 | "StorybookMacrosPlugin", 29 | .product(name: "SwiftUISupport", package: "swiftui-support"), 30 | "ResultBuilderKit", 31 | ] 32 | ), 33 | .macro( 34 | name: "StorybookMacrosPlugin", 35 | dependencies: [ 36 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 37 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 38 | ] 39 | ), 40 | .testTarget( 41 | name: "StorybookMacrosTests", 42 | dependencies: [ 43 | "StorybookMacrosPlugin", 44 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 45 | .product(name: "MacroTesting", package: "swift-macro-testing"), 46 | ] 47 | ), 48 | .target( 49 | name: "StorybookKitTextureSupport", 50 | dependencies: [ 51 | .product(name: "TextureSwiftSupport", package: "TextureSwiftSupport"), 52 | .product(name: "TextureBridging", package: "TextureBridging"), 53 | "StorybookKit", 54 | ] 55 | ), 56 | ], 57 | swiftLanguageModes: [.v6, .v5] 58 | ) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Storybook for iOS 2 | 3 | This library allows you to view UI components in a catalog-style format. 4 | In most cases, it works by simply adding a few lines of code, as it gathers SwiftUI preview codes at runtime. 5 | 6 | storybook previewing 7 | 8 | ## Setup 9 | 10 | 1. Install this package into your project. 11 | 12 | 2. Put the entrypoint view. 13 | 14 | ```swift 15 | import StorybookKit 16 | import SwiftUI 17 | 18 | struct ContentView: View { 19 | var body: some View { 20 | Storybook() 21 | } 22 | } 23 | ``` 24 | 25 | ## Example 26 | 27 | In app executable module 28 | 29 | ```swift 30 | #Preview("Circle") { 31 | Circle() 32 | .fill(.purple) 33 | .frame(width: 100, height: 100) 34 | } 35 | ``` 36 | 37 | In a dynamic framework module 38 | ```swift 39 | #Preview("Circle") { 40 | Circle() 41 | .fill(.purple) 42 | .frame(width: 100, height: 100) 43 | } 44 | ``` 45 | 46 | In a static library module 47 | ```swift 48 | #Preview("Circle") { 49 | Circle() 50 | .fill(.purple) 51 | .frame(width: 100, height: 100) 52 | } 53 | ``` 54 | 55 | > [!IMPORTANT] 56 | > To display all preview codes in a statically linked binary, you may need to link the binary with the -all_load linker flag. 57 | > This is because the linker does not load symbols into the target binary if it deems them unnecessary. 58 | 59 | list of modules 60 | 61 | ## License 62 | 63 | Storybook-ios is released under the MIT license. 64 | 65 | 66 | -------------------------------------------------------------------------------- /Sources/StorybookKit/BookGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import UIKit 23 | import Foundation 24 | 25 | public enum BookGenerator { 26 | 27 | public static func randomColor() -> UIColor { 28 | let red = CGFloat(Int.random(in: 0 ... 255)) 29 | let green = CGFloat(Int.random(in: 0 ... 255)) 30 | let blue = CGFloat(Int.random(in: 0 ... 255)) 31 | return UIColor.init(red: red / 255, green: green / 255, blue: blue / 255, alpha: 1.0) 32 | } 33 | 34 | public static func loremIpsum(length: Int) -> String { 35 | BookLorem.ipsum(length) 36 | } 37 | 38 | public static func randomEmoji() -> String { 39 | let range = 0x1F601...0x1F64F 40 | let ascii = range.lowerBound + Int(arc4random_uniform(UInt32(range.count))) 41 | 42 | var view = String.UnicodeScalarView() 43 | view.append(UnicodeScalar(ascii)!) 44 | 45 | let emoji = String(view) 46 | 47 | return emoji 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sources/StorybookKit/BookLorem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | // MARK: - Lorem 25 | public enum BookLorem { 26 | // MARK: Public 27 | 28 | public static func ipsum(_ length: Int) -> String { 29 | 30 | var string = lorem 31 | while string.count < length { 32 | 33 | string += ("\n" + lorem) 34 | } 35 | return String(string.prefix(length)) 36 | } 37 | 38 | // MARK: Private 39 | private static let lorem: String = """ 40 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sodales tristique elit et fringilla. Etiam eget justo non libero rhoncus placerat. Ut augue enim, gravida vitae consectetur ac, porta sit amet justo. Proin sed tristique dui. Vivamus at justo iaculis, dapibus tellus at, vestibulum arcu. Mauris et augue dictum ex rhoncus ultrices. Morbi ac diam eget leo consequat sodales. Phasellus sed luctus sem. Vestibulum sit amet dapibus nulla. Vivamus facilisis non dolor sed ornare. 41 | 42 | Phasellus malesuada diam ac magna feugiat, et malesuada ligula euismod. Donec at commodo ipsum. Etiam nisl lorem, commodo sed condimentum non, mattis accumsan tellus. Donec mollis, quam nec euismod commodo, mi mi dapibus ipsum, sit amet molestie leo mi non erat. Curabitur nec porttitor libero. Sed a rutrum sem. Quisque non tellus aliquet, bibendum elit nec, porta ante. Aenean nibh sapien, euismod id dapibus eu, auctor a nulla. Quisque ultricies sollicitudin dolor, sit amet lobortis justo pellentesque nec. Nulla facilisi. Duis rutrum orci pellentesque felis ultricies venenatis. Donec ullamcorper dui nisl, nec auctor lacus consectetur sit amet. Ut libero ipsum, por ttitor non pellentesque volutpat, accumsan at erat. Quisque at tempor libero. Ut aliquet sem at sem dictum commodo. 43 | 44 | Curabitur commodo varius finibus. Vivamus vestibulum sodales orci, ut consequat augue vestibulum quis. Nunc tempus vulputate dapibus. In euismod, risus at congue rhoncus, magna diam ornare enim, eget semper nunc ipsum vel nisl. Nulla tincidunt, lorem sit amet imperdiet aliquam, magna arcu gravida quam, eu malesuada purus nulla quis odio. Maecenas quis eros tincidunt, tempor leo volutpat, commodo ligula. Vivamus vulputate massa at tellus hendrerit, non congue lacus ullamcorper. Aenean sit amet sem nec nunc molestie mattis. Nam facilisis rutrum mi in mattis. In nec efficitur turpis, sed hendrerit ipsum. Nunc et fermentum tellus. Sed aliquet ligula at elit dictum, eget eleifend leo auctor. Morbi tempus finibus est vel rhoncus. Integer feugiat porttitor justo. Aliquam at nisi quis lorem aliquet faucibus. 45 | 46 | Fusce sodales facilisis felis sed pretium. Nullam arcu nunc, malesuada sit amet mollis eu, semper eu quam. Suspendisse est tellus, tincidunt nec purus vel, ultricies condimentum nulla. Praesent purus arcu, maximus at commodo id, volutpat id erat. Mauris nec erat nec erat vestibulum ultrices sed in dolor. Aliquam lacus ante, feugiat sit amet sapien id, semper pellentesque lacus. Morbi justo massa, molestie nec nulla a, hendrerit vehicula odio. Etiam sollicitudin est quis urna convallis, non maximus quam pharetra. 47 | 48 | Donec at leo a lectus dapibus commodo eu id dui. Cras tristique mauris sed pharetra congue. Duis nisl magna, mollis eget volutpat et, dignissim at neque. Sed in enim eu nisi fermentum hendrerit. Pellentesque sollicitudin, nibh eu auctor iaculis, quam mi pulvinar metus, eget posuere elit dolor ac dolor. Quisque ultricies consequat magna eu semper. Nulla placerat volutpat augue, ut tempus mi tincidunt id. Nunc in sollicitudin orci. Sed at justo at metus luctus scelerisque at sed arcu. Maecenas pulvinar dui a consequat rutrum. Etiam interdum ligula felis. In eu auctor magna, finibus aliquam massa. Integer finibus quam tellus, quis porta metus vehicula vitae. Aliquam id odio interdum, consectetur augue vitae, interdum dolor. 49 | """ 50 | } 51 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Compositions/BookPadding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | public struct BookSpacer: BookView { 25 | 26 | public let height: CGFloat 27 | 28 | public init(height: CGFloat) { 29 | self.height = height 30 | } 31 | 32 | public var bookBody: any BookView { 33 | fatalError() 34 | } 35 | 36 | public var body: some View { 37 | Spacer(minLength: 0) 38 | .frame(height: height) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Compositions/BookPattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import SwiftUI 24 | import SwiftUISupport 25 | 26 | public struct BookPattern { 27 | 28 | public let patterns: [Element] 29 | 30 | init(_ source: [Element]) { 31 | self.patterns = source 32 | } 33 | 34 | public func makeBody(@ViewBuilder _ content: @escaping (Element) -> Content) -> some View { 35 | ForEach.inefficient(items: patterns) { pattern in 36 | content(pattern) 37 | } 38 | } 39 | } 40 | 41 | extension BookPattern where Element == Any { 42 | 43 | public static func make(_ p0: P0) -> BookPattern<(P0.Element)> { 44 | .init(p0.map { $0 }) 45 | } 46 | 47 | public static func make(_ p0: P0, _ p1: P1) -> BookPattern<(P0.Element, P1.Element)> { 48 | 49 | var buffer: [(P0.Element, P1.Element)] = [] 50 | 51 | p0.forEach { p0 in 52 | p1.forEach { p1 in 53 | buffer.append((p0, p1)) 54 | } 55 | } 56 | 57 | return .init(buffer) 58 | 59 | } 60 | 61 | public static func make(_ p0: P0, _ p1: P1, _ p2: P2) -> BookPattern<(P0.Element, P1.Element, P2.Element)> { 62 | 63 | var buffer: [(P0.Element, P1.Element, P2.Element)] = [] 64 | 65 | p0.forEach { p0 in 66 | p1.forEach { p1 in 67 | p2.forEach { p2 in 68 | buffer.append((p0, p1, p2)) 69 | } 70 | } 71 | } 72 | 73 | return .init(buffer) 74 | 75 | } 76 | 77 | public static func make(_ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3) -> BookPattern<(P0.Element, P1.Element, P2.Element, P3.Element)> { 78 | 79 | var buffer: [(P0.Element, P1.Element, P2.Element, P3.Element)] = [] 80 | 81 | p0.forEach { p0 in 82 | p1.forEach { p1 in 83 | p2.forEach { p2 in 84 | p3.forEach { p3 in 85 | buffer.append((p0, p1, p2, p3)) 86 | } 87 | } 88 | } 89 | } 90 | 91 | return .init(buffer) 92 | 93 | } 94 | 95 | public static func make(_ p0: P0, _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4) -> BookPattern<(P0.Element, P1.Element, P2.Element, P3.Element, P4.Element)> { 96 | 97 | var buffer: [(P0.Element, P1.Element, P2.Element, P3.Element, P4.Element)] = [] 98 | 99 | p0.forEach { p0 in 100 | p1.forEach { p1 in 101 | p2.forEach { p2 in 102 | p3.forEach { p3 in 103 | p4.forEach { p4 in 104 | buffer.append((p0, p1, p2, p3, p4)) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | return .init(buffer) 112 | 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Compositions/BookSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | public struct BookSection: BookView { 25 | 26 | public let title: String 27 | public let content: Content 28 | 29 | public init( 30 | title: String, 31 | @ViewBuilder content: () -> Content 32 | ) { 33 | self.title = title 34 | self.content = content() 35 | } 36 | 37 | public var body: some View { 38 | BookText(title) 39 | .font(.system(size: 24, weight: .bold)) 40 | content 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Internals/Hosting/_ViewControllerHost.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct _ViewControllerHost: 4 | UIViewControllerRepresentable 5 | { 6 | 7 | private let instantiate: @MainActor () -> ContentViewController 8 | private let _update: 9 | @MainActor (_ uiViewController: ContentViewController, _ context: Context) -> Void 10 | 11 | init( 12 | instantiate: @escaping @MainActor () -> ContentViewController, 13 | update: @escaping @MainActor (_ uiViewController: ContentViewController, _ context: Context) -> 14 | Void = { _, _ in } 15 | ) { 16 | self.instantiate = instantiate 17 | self._update = update 18 | } 19 | 20 | func makeUIViewController(context: Context) -> ContentViewController { 21 | let instantiated = instantiate() 22 | return instantiated 23 | } 24 | 25 | func updateUIViewController(_ uiViewController: ContentViewController, context: Context) { 26 | _update(uiViewController, context) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Internals/Hosting/_ViewHost.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct _ViewHost: 4 | UIViewRepresentable 5 | { 6 | 7 | private let instantiate: @MainActor () -> ContentView 8 | private let _update: @MainActor (_ uiView: ContentView, _ context: Context) -> Void 9 | private let maxWidth: Double 10 | 11 | init( 12 | maxWidth: Double, 13 | instantiate: @escaping @MainActor () -> ContentView, 14 | update: @escaping @MainActor (_ uiViewController: ContentView, _ context: Context) -> 15 | Void = { _, _ in } 16 | ) { 17 | self.maxWidth = maxWidth 18 | self.instantiate = instantiate 19 | self._update = update 20 | } 21 | 22 | func makeUIView(context: Context) -> _Label { 23 | let instantiated = instantiate() 24 | return .init(maxWidth: maxWidth, contentView: instantiated) 25 | } 26 | 27 | func updateUIView(_ uiView: _Label, context: Context) { 28 | _update(uiView.contentView, context) 29 | } 30 | 31 | } 32 | 33 | final class _Label: UILabel { 34 | 35 | let contentView: ContentView 36 | let maxWidth: Double 37 | 38 | init( 39 | maxWidth: Double, 40 | contentView: ContentView 41 | ) { 42 | self.maxWidth = maxWidth 43 | self.contentView = contentView 44 | super.init(frame: .null) 45 | addSubview(contentView) 46 | numberOfLines = 0 47 | isUserInteractionEnabled = true 48 | } 49 | 50 | required init?(coder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) 55 | -> CGRect 56 | { 57 | 58 | var targetSize = bounds.size 59 | 60 | if targetSize.width == CGFloat(Float.greatestFiniteMagnitude) { 61 | targetSize.width = UIView.layoutFittingCompressedSize.width 62 | } 63 | 64 | if targetSize.height == CGFloat(Float.greatestFiniteMagnitude) { 65 | targetSize.height = UIView.layoutFittingCompressedSize.height 66 | } 67 | 68 | var size = contentView.systemLayoutSizeFitting(targetSize) 69 | 70 | if size.width > maxWidth { 71 | targetSize.width = maxWidth 72 | size = contentView.systemLayoutSizeFitting( 73 | targetSize, 74 | withHorizontalFittingPriority: .required, 75 | verticalFittingPriority: .fittingSizeLevel 76 | ) 77 | } 78 | 79 | return .init(origin: .zero, size: size) 80 | } 81 | 82 | override func layoutSubviews() { 83 | super.layoutSubviews() 84 | 85 | contentView.frame = bounds 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Internals/Preview/PreviewRegistryWrapper.swift: -------------------------------------------------------------------------------- 1 | import DeveloperToolsSupport 2 | import Foundation 3 | import SwiftUI 4 | import UIKit 5 | 6 | @available(iOS 17.0, *) 7 | struct PreviewRegistryWrapper: Comparable { 8 | 9 | let previewType: any DeveloperToolsSupport.PreviewRegistry.Type 10 | let module: String 11 | 12 | init(_ previewType: any DeveloperToolsSupport.PreviewRegistry.Type) { 13 | self.previewType = previewType 14 | self.module = previewType.fileID.components(separatedBy: "/").first! 15 | } 16 | 17 | var fileID: String { previewType.fileID } 18 | var line: Int { previewType.line } 19 | var column: Int { previewType.column } 20 | 21 | @MainActor 22 | var displayName: String? { 23 | guard let rawPreview = try? previewType.makePreview() else { 24 | return nil 25 | } 26 | let preview: FieldReader = .init(rawPreview) 27 | return preview["displayName"] 28 | } 29 | 30 | @MainActor 31 | var makeView: (@MainActor () -> any View) { 32 | guard let rawPreview = try? previewType.makePreview() else { 33 | return { EmptyView() } 34 | } 35 | let preview: FieldReader = .init(rawPreview) 36 | let title: String? = preview["displayName"] 37 | let source: FieldReader = preview["source"] 38 | switch source.typeName { 39 | 40 | case "SwiftUI.ViewPreviewSource": // iOS 17 41 | let makeView: MakeFunctionWrapper = .init(source["makeView"]) 42 | return { 43 | VStack { 44 | if let title, !title.isEmpty { 45 | Text(title) 46 | .font(.system(size: 17, weight: .semibold)) 47 | } 48 | AnyView(makeView()) 49 | Text("\(fileID):\(line)") 50 | .font(.caption.monospacedDigit()) 51 | BookSpacer(height: 16) 52 | } 53 | } 54 | 55 | case "UIKit.UIViewPreviewSource": // iOS 17 56 | // Unsupported due to iOS 17 not supporting casting between non-sendable closure types 57 | return { 58 | VStack { 59 | if let title, !title.isEmpty { 60 | Text(title) 61 | .font(.system(size: 17, weight: .semibold)) 62 | } 63 | Text("UIView Preview not supported on iOS 17") 64 | .foregroundStyle(Color.red) 65 | .font(.caption.monospacedDigit()) 66 | Text("\(fileID):\(line)") 67 | .font(.caption.monospacedDigit()) 68 | BookSpacer(height: 16) 69 | } 70 | } 71 | 72 | case "UIKit.UIViewControllerPreviewSource": // iOS 17 73 | // Unsupported due to iOS 17 not supporting casting between non-sendable closure types 74 | return { 75 | VStack { 76 | if let title, !title.isEmpty { 77 | Text(title) 78 | .font(.system(size: 17, weight: .semibold)) 79 | } 80 | Text("UIViewController Preview not supported on iOS 17") 81 | .foregroundStyle(Color.red) 82 | .font(.caption.monospacedDigit()) 83 | Text("\(fileID):\(line)") 84 | .font(.caption.monospacedDigit()) 85 | BookSpacer(height: 16) 86 | } 87 | } 88 | 89 | case "DeveloperToolsSupport.DefaultPreviewSource": // iOS 18 90 | let makeBody: MakeFunctionWrapper = .init(source["structure", "singlePreview", "makeBody"]) 91 | return { 92 | VStack { 93 | AnyView(makeBody()) 94 | .toolbar { 95 | ToolbarItem(placement: .topBarTrailing) { 96 | Menu { 97 | Button { 98 | UIPasteboard.general.string = "\(fileID):\(line)" 99 | } label: { 100 | Text("\(fileID):\(line)") 101 | .font(.caption.monospacedDigit()) 102 | } 103 | } label: { 104 | Image(systemName: "info.circle") 105 | } 106 | 107 | } 108 | } 109 | } 110 | } 111 | 112 | case "DeveloperToolsSupport.DefaultPreviewSource<__C.UIView>": // iOS 18 113 | let makeBody: MakeFunctionWrapper = .init(source["structure", "singlePreview", "makeBody"]) 114 | return { 115 | BookPreview( 116 | fileID, 117 | line, 118 | title: title ?? source.typeName, 119 | viewBlock: { _ in 120 | makeBody() 121 | } 122 | ) 123 | } 124 | 125 | case "DeveloperToolsSupport.DefaultPreviewSource<__C.UIViewController>": // iOS 18 126 | let makeBody: MakeFunctionWrapper = .init(source["structure", "singlePreview", "makeBody"]) 127 | return { 128 | BookPresent( 129 | title: title ?? source.typeName, 130 | presentingViewControllerBlock: { 131 | makeBody() 132 | } 133 | ) 134 | } 135 | 136 | case let sourceTypeName: 137 | return { 138 | VStack { 139 | if let title, !title.isEmpty { 140 | Text(title) 141 | .font(.system(size: 17, weight: .semibold)) 142 | } 143 | Text("Failed to load preview (\(sourceTypeName))") 144 | .foregroundStyle(Color.red) 145 | .font(.caption.monospacedDigit()) 146 | Text("\(fileID):\(line)") 147 | .font(.caption.monospacedDigit()) 148 | BookSpacer(height: 16) 149 | } 150 | } 151 | } 152 | } 153 | 154 | 155 | // MARK: Comparable 156 | 157 | static func < (lhs: PreviewRegistryWrapper, rhs: PreviewRegistryWrapper) -> Bool { 158 | if lhs.module == rhs.module { 159 | return lhs.line < rhs.line 160 | } 161 | return lhs.module < rhs.module 162 | } 163 | 164 | 165 | // MARK: Equatable 166 | 167 | static func == (lhs: PreviewRegistryWrapper, rhs: PreviewRegistryWrapper) -> Bool { 168 | lhs.line == rhs.line && lhs.module == rhs.module 169 | } 170 | 171 | 172 | // MARK: - FieldReader 173 | 174 | private struct FieldReader { 175 | 176 | let instance: Any 177 | let typeName: String 178 | 179 | init(_ instance: Any) { 180 | self.instance = instance 181 | self.typeName = String(reflecting: type(of: instance)) 182 | let mirror: Mirror = .init(reflecting: instance) 183 | self.fields = .init( 184 | uniqueKeysWithValues: mirror.children.compactMap { (label, value) in 185 | label.map({ ($0, value) }) 186 | } 187 | ) 188 | } 189 | 190 | subscript(_ key: String, _ nextKeys: String...) -> T { 191 | if nextKeys.isEmpty { 192 | return fields[key]! as! T 193 | } 194 | else { 195 | return Self.traverse(from: fields[key]!, nextKeys: nextKeys) as! T 196 | } 197 | } 198 | 199 | subscript(_ key: String, _ nextKeys: String...) -> FieldReader { 200 | .init(Self.traverse(from: fields[key]!, nextKeys: nextKeys)) 201 | } 202 | 203 | private let fields: [String: Any] 204 | 205 | private static func traverse>(from first: Any, nextKeys: C) -> Any { 206 | if let key = nextKeys.first { 207 | let mirror: Mirror = .init(reflecting: first) 208 | return self.traverse( 209 | from: mirror.children.first(where: { $0.label == key })!.value, 210 | nextKeys: nextKeys.dropFirst() 211 | ) 212 | } 213 | else { 214 | return first 215 | } 216 | } 217 | } 218 | 219 | 220 | // MARK: - MakeFunctionWrapper 221 | 222 | @MainActor 223 | private struct MakeFunctionWrapper { 224 | 225 | typealias Closure = @MainActor () -> T 226 | private let closure: Closure 227 | 228 | init(_ closure: Any) { 229 | // TODO: We need a workaround to avoid implicit @Sendable from @MainActor closures 230 | self.closure = unsafeBitCast( 231 | closure, 232 | to: Closure.self 233 | ) 234 | } 235 | 236 | func callAsFunction() -> T { 237 | closure() 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Internals/TargetViewControllerKey.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension EnvironmentValues { 4 | @Entry public var storybook_targetViewController: UIViewController? 5 | } 6 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Internals/fuzzyMatch.swift: -------------------------------------------------------------------------------- 1 | private extension String { 2 | 3 | func charAt(_ i: Int) -> Character { 4 | let index = self.index(self.startIndex, offsetBy: i) 5 | return self[index] 6 | } 7 | 8 | func charStrAt(_ i: Int) -> String { 9 | return String(charAt(i)) 10 | } 11 | } 12 | 13 | extension String { 14 | 15 | func score(word: String, fuzziness: Double? = nil) -> Double { 16 | 17 | // If the string is equal to the word, perfect match. 18 | if self == word { 19 | return 1 20 | } 21 | 22 | //if it's not a perfect match and is empty return 0 23 | if word.isEmpty || self.isEmpty { 24 | return 0 25 | } 26 | 27 | var runningScore = 0.0 28 | var charScore = 0.0 29 | var finalScore = 0.0 30 | 31 | let string = self 32 | let lString = string.lowercased() 33 | let strLength = lString.count 34 | let lWord = word.lowercased() 35 | let wordLength = word.count 36 | 37 | var idxOf: String.Index! 38 | var startAt = lString.startIndex 39 | var fuzzies = 1.0 40 | var fuzzyFactor = 0.0 41 | var fuzzinessIsNil = true 42 | 43 | // Cache fuzzyFactor for speed increase 44 | if let fuzziness = fuzziness { 45 | fuzzyFactor = 1 - fuzziness 46 | fuzzinessIsNil = false 47 | } 48 | 49 | for i in 0 ..< wordLength { 50 | // Find next first case-insensitive match of word's i-th character. 51 | // The search in "string" begins at "startAt". 52 | 53 | if let range = lString.range( 54 | of: lWord.charStrAt(i), 55 | options: [.caseInsensitive, .diacriticInsensitive], 56 | range: startAt.. [any BookProvider.Type]? { 35 | let moduleName = Bundle.main.bundleURL.deletingPathExtension().lastPathComponent 36 | guard !moduleName.isEmpty else { 37 | return nil 38 | } 39 | var results: [any BookProvider.Type] = [] 40 | for imageIndex in 0 ..< _dyld_image_count() { 41 | self.findAllBookProviders( 42 | inImageIndex: .init(imageIndex), 43 | filterByStorybookPageMacro: filterByStorybookPageMacro, 44 | results: &results 45 | ) 46 | } 47 | return results 48 | } 49 | 50 | @available(iOS 17.0, *) 51 | static func findAllPreviews( 52 | excludeStorybookPageMacro: Bool = true 53 | ) -> [PreviewRegistryWrapper]? { 54 | let moduleName = Bundle.main.bundleURL.deletingPathExtension().lastPathComponent 55 | guard !moduleName.isEmpty else { 56 | return nil 57 | } 58 | var results: [PreviewRegistryWrapper] = [] 59 | for imageIndex in 0 ..< _dyld_image_count() { 60 | self.findAllPreviews( 61 | inImageIndex: .init(imageIndex), 62 | excludeStorybookPageMacro: excludeStorybookPageMacro, 63 | results: &results 64 | ) 65 | } 66 | return results 67 | } 68 | 69 | private static func findAllBookProviders( 70 | inImageIndex imageIndex: UInt32, 71 | filterByStorybookPageMacro: Bool, 72 | results: inout [any BookProvider.Type] 73 | ) { 74 | // Follows same approach here: https://github.com/apple/swift-testing/blob/main/Sources/TestingInternals/Discovery.cpp#L318 75 | guard 76 | let headerRawPtr: UnsafeRawPointer = _dyld_get_image_header(imageIndex) 77 | .map(UnsafeRawPointer.init(_:)) 78 | else { 79 | return 80 | } 81 | let headerPtr = headerRawPtr.assumingMemoryBound( 82 | to: mach_header_64.self 83 | ) 84 | // https://derekselander.github.io/dsdump/ 85 | var size: UInt = 0 86 | guard 87 | let sectionRawPtr = getsectiondata( 88 | headerPtr, 89 | SEG_TEXT, 90 | "__swift5_types", 91 | &size 92 | ) 93 | .map(UnsafeRawPointer.init(_:)) 94 | else { 95 | return 96 | } 97 | let capacity: Int = .init(size) / MemoryLayout.size 98 | let sectionPtr = sectionRawPtr.assumingMemoryBound( 99 | to: SwiftTypeMetadataRecord.self 100 | ) 101 | for index in 0 ..< capacity { 102 | let record = sectionPtr.advanced(by: index) 103 | guard 104 | let contextDescriptor = record.pointee.contextDescriptor( 105 | from: record 106 | ) 107 | else { 108 | continue 109 | } 110 | guard !contextDescriptor.pointee.isGeneric() else { 111 | continue 112 | } 113 | guard 114 | contextDescriptor.pointee.kind().canConformToProtocol, 115 | !filterByStorybookPageMacro || contextDescriptor.nameContains(self._magicSubstring) 116 | else { 117 | continue 118 | } 119 | let metadataClosure = contextDescriptor.resolveValue(for: \.metadataAccessFunction) 120 | let metadata = metadataClosure(0xFF) 121 | guard 122 | let metadataAccessFunction = metadata.value 123 | else { 124 | continue 125 | } 126 | let anyType = unsafeBitCast( 127 | metadataAccessFunction, 128 | to: Any.Type.self 129 | ) 130 | guard let bookProviderType = anyType as? any BookProvider.Type else { 131 | continue 132 | } 133 | results.append(bookProviderType) 134 | } 135 | } 136 | 137 | @available(iOS 17.0, *) 138 | private static func findAllPreviews( 139 | inImageIndex imageIndex: UInt32, 140 | excludeStorybookPageMacro: Bool, 141 | results: inout [PreviewRegistryWrapper] 142 | ) { 143 | // Follows same approach here: https://github.com/apple/swift-testing/blob/main/Sources/TestingInternals/Discovery.cpp#L318 144 | guard 145 | let headerRawPtr: UnsafeRawPointer = _dyld_get_image_header(imageIndex) 146 | .map(UnsafeRawPointer.init(_:)) 147 | else { 148 | return 149 | } 150 | let headerPtr = headerRawPtr.assumingMemoryBound( 151 | to: mach_header_64.self 152 | ) 153 | // https://derekselander.github.io/dsdump/ 154 | var size: UInt = 0 155 | guard 156 | let sectionRawPtr = getsectiondata( 157 | headerPtr, 158 | SEG_TEXT, 159 | "__swift5_types", 160 | &size 161 | ) 162 | .map(UnsafeRawPointer.init(_:)) 163 | else { 164 | return 165 | } 166 | let capacity: Int = .init(size) / MemoryLayout.size 167 | let sectionPtr = sectionRawPtr.assumingMemoryBound( 168 | to: SwiftTypeMetadataRecord.self 169 | ) 170 | for index in 0 ..< capacity { 171 | let record = sectionPtr.advanced(by: index) 172 | guard 173 | let contextDescriptor = record.pointee.contextDescriptor( 174 | from: record 175 | ) 176 | else { 177 | continue 178 | } 179 | guard !contextDescriptor.pointee.isGeneric() else { 180 | continue 181 | } 182 | guard 183 | case .structType = contextDescriptor.pointee.kind(), 184 | contextDescriptor.nameContains("PreviewRegistry"), 185 | !excludeStorybookPageMacro || !contextDescriptor.nameContains(self._magicSubstring) 186 | else { 187 | continue 188 | } 189 | let metadataClosure = contextDescriptor.resolveValue(for: \.metadataAccessFunction) 190 | let metadata = metadataClosure(0xFF) 191 | guard 192 | let metadataAccessFunction = metadata.value 193 | else { 194 | continue 195 | } 196 | let anyType = unsafeBitCast( 197 | metadataAccessFunction, 198 | to: Any.Type.self 199 | ) 200 | guard 201 | let previewType = anyType as? DeveloperToolsSupport.PreviewRegistry.Type 202 | else { 203 | continue 204 | } 205 | results.append(.init(previewType)) 206 | } 207 | } 208 | } 209 | 210 | extension UnsafePointer where Pointee: SwiftLayoutPointer { 211 | 212 | fileprivate func resolvePointer(for keyPath: KeyPath>) -> UnsafePointer { 213 | let base: UnsafeRawPointer = .init(self) 214 | let fieldOffset = MemoryLayout.offset(of: keyPath)! 215 | let relativePointer = self.pointee[keyPath: keyPath] 216 | return base 217 | .advanced(by: fieldOffset) 218 | .advanced(by: .init(relativePointer.offset)) 219 | .assumingMemoryBound(to: U.self) 220 | } 221 | 222 | fileprivate func resolveValue(for keyPath: KeyPath>) -> U { 223 | let base: UnsafeRawPointer = .init(self) 224 | let fieldOffset = MemoryLayout.offset(of: keyPath)! 225 | let relativePointer = self.pointee[keyPath: keyPath] 226 | let pointer = base 227 | .advanced(by: fieldOffset) 228 | .advanced(by: .init(relativePointer.offset)) 229 | return unsafeBitCast(pointer, to: U.self) 230 | } 231 | } 232 | 233 | extension UnsafePointer where Pointee == SwiftTypeContextDescriptor { 234 | fileprivate func nameContains(_ string: String) -> Bool { 235 | string.withCString( 236 | { 237 | let nameCString = self.resolvePointer(for: \.name) 238 | return nil != strstr(nameCString, $0) 239 | } 240 | ) 241 | } 242 | } 243 | 244 | fileprivate protocol SwiftLayoutPointer { 245 | static var maskValue: Int32 { get } 246 | } 247 | 248 | extension SwiftLayoutPointer { 249 | fileprivate static var maskValue: Int32 { 250 | return 0 251 | } 252 | } 253 | 254 | fileprivate struct SwiftRelativePointer { 255 | 256 | var offset: Int32 = 0 257 | 258 | func pointer( 259 | from base: UnsafeRawPointer, 260 | vmAddrSlide: Int 261 | ) -> UnsafePointer? { 262 | let maskedOffset: Int = .init(offset) 263 | guard maskedOffset != 0 else { 264 | return nil 265 | } 266 | return base 267 | .advanced(by: -vmAddrSlide) 268 | .advanced(by: maskedOffset) 269 | .assumingMemoryBound(to: T.self) 270 | } 271 | } 272 | 273 | extension SwiftRelativePointer where T: SwiftLayoutPointer { 274 | 275 | func value() -> Int32 { 276 | offset & T.maskValue 277 | } 278 | 279 | func pointer( 280 | from base: UnsafeRawPointer 281 | ) -> UnsafePointer? { 282 | let maskedOffset: Int = .init(offset & ~T.maskValue) 283 | guard maskedOffset != 0 else { 284 | return nil 285 | } 286 | return base 287 | .advanced(by: maskedOffset) 288 | .assumingMemoryBound(to: T.self) 289 | } 290 | } 291 | 292 | fileprivate struct SwiftTypeMetadataRecord: SwiftLayoutPointer { 293 | 294 | var pointer: SwiftRelativePointer 295 | 296 | func contextDescriptor( 297 | from base: UnsafeRawPointer 298 | ) -> UnsafePointer? { 299 | switch pointer.value() { 300 | case 0: 301 | return pointer.pointer(from: base) 302 | 303 | case 1: 304 | // Untested 305 | return pointer.pointer(from: base).map { 306 | let indirection: UnsafeRawPointer = .init($0) 307 | return indirection 308 | .assumingMemoryBound(to: UnsafePointer.self) 309 | .pointee 310 | } 311 | 312 | default: 313 | return nil 314 | } 315 | } 316 | } 317 | 318 | fileprivate struct SwiftTypeContextDescriptor: SwiftLayoutPointer { 319 | var flags: UInt32 = 0 320 | var parent: SwiftRelativePointer = .init() 321 | var name: SwiftRelativePointer = .init() 322 | var metadataAccessFunction: SwiftRelativePointer<(@convention(thin) (Int) -> MetadataAccessResponse)> = .init() 323 | 324 | func isGeneric() -> Bool { 325 | return (flags & 0x80) != 0 326 | } 327 | 328 | func kind() -> Kind { 329 | // https://github.com/blacktop/go-macho/blob/master/types/swift/types.go#L589 330 | return .init(rawValue: flags & 0x1F) 331 | } 332 | 333 | struct MetadataAccessResponse: SwiftLayoutPointer { 334 | var value: UnsafeRawPointer? 335 | var state: Int = 0 336 | } 337 | 338 | static let maskValue: Int32 = .init(MemoryLayout.alignment - 1) 339 | 340 | struct Kind: RawRepresentable, Equatable { 341 | static let module: Self = .init(rawValue: 0) 342 | static let `extension`: Self = .init(rawValue: 1) 343 | static let anonymous: Self = .init(rawValue: 2) 344 | static let `protocol`: Self = .init(rawValue: 3) 345 | static let opaqueType: Self = .init(rawValue: 4) 346 | 347 | static let typesStart: Self = .init(rawValue: 16) 348 | 349 | static let classType: Self = .init(rawValue: Self.typesStart.rawValue) 350 | static let structType: Self = .init(rawValue: Self.typesStart.rawValue + 1) 351 | static let enumType: Self = .init(rawValue: Self.typesStart.rawValue + 2) 352 | 353 | static let typesEnd: Self = .init(rawValue: 31) 354 | 355 | let rawValue: UInt32 356 | 357 | init(rawValue: UInt32) { 358 | self.rawValue = rawValue 359 | } 360 | 361 | var canConformToProtocol: Bool { 362 | return (Self.typesStart.rawValue ... Self.typesEnd.rawValue).contains(self.rawValue) 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/Book.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct Book: BookView, Identifiable { 4 | 5 | public enum Node: Identifiable { 6 | 7 | public var id: String { 8 | switch self { 9 | case .folder(let v): return "folder.\(v.id)" 10 | case .page(let v): return "page.\(v.id)" 11 | } 12 | } 13 | 14 | case folder(Book) 15 | case page(BookPage) 16 | 17 | var sortingKey: String { 18 | switch self { 19 | case .folder(let v): return v.title 20 | case .page(let v): return v.title 21 | } 22 | } 23 | 24 | @MainActor 25 | var children: [Node]? { 26 | switch self { 27 | case .folder(let v): return v.contents 28 | case .page: return nil 29 | } 30 | } 31 | 32 | } 33 | 34 | public var id: UUID = .init() 35 | 36 | public let title: String 37 | public let contents: [Node] 38 | 39 | @State private var isExpandedAll: Bool = false 40 | 41 | public init( 42 | title: String, 43 | @FolderBuilder contents: () -> [Node] 44 | ) { 45 | 46 | self.title = title 47 | 48 | let _contents = contents() 49 | 50 | let folders = 51 | _contents 52 | .filter { 53 | switch $0 { 54 | case .folder: 55 | return true 56 | case .page: 57 | return false 58 | } 59 | } 60 | .sorted { a, b in 61 | a.sortingKey < b.sortingKey 62 | } 63 | 64 | let pages = 65 | _contents 66 | .filter { 67 | switch $0 { 68 | case .folder: 69 | return false 70 | case .page: 71 | return true 72 | } 73 | } 74 | .sorted { a, b in 75 | a.sortingKey < b.sortingKey 76 | } 77 | 78 | self.contents = folders + pages 79 | 80 | } 81 | 82 | public var body: some View { 83 | Section { 84 | ForEach(contents) { node in 85 | NodeOutlineGroup(expandsAll: isExpandedAll, node, children: \.children) { content in 86 | switch content { 87 | case .folder(let folder): 88 | 89 | HStack { 90 | Image.init(systemName: "folder.fill") 91 | Text(folder.title) 92 | } 93 | 94 | case .page(let page): 95 | page 96 | } 97 | } 98 | 99 | } 100 | } header: { 101 | HStack { 102 | Text(title) 103 | Spacer() 104 | Button(isExpandedAll ? " Collapse All" : " Expand All") { 105 | isExpandedAll.toggle() 106 | } 107 | .font(.caption) 108 | } 109 | } 110 | 111 | } 112 | 113 | func allPages() -> [BookPage] { 114 | contents.flatMap { node -> [BookPage] in 115 | switch node { 116 | case .folder(let folder): 117 | return folder.allPages() 118 | case .page(let page): 119 | return [page] 120 | } 121 | } 122 | } 123 | 124 | } 125 | 126 | // MARK: Runtime creation 127 | extension Book { 128 | 129 | /// All conformers to `BookProvider`, including those declared from the `#StorybookPage` macro 130 | public static func allBookProviders() -> [any BookProvider.Type] { 131 | self.findAllBookProviders(filterByStorybookPageMacro: false) ?? [] 132 | } 133 | 134 | /// All conformers to `BookProvider` that were declared from the `#StorybookPage` macro 135 | public static func allStorybookPages() -> [any BookProvider.Type] { 136 | self.findAllBookProviders(filterByStorybookPageMacro: true) ?? [] 137 | } 138 | 139 | /// All `#Preview`s as `BookPage`s 140 | @available(iOS 17.0, *) 141 | public static func allBookPreviews() -> [Node]? { 142 | guard let sortedPreviewRegistries = self.findAllPreviews() else { 143 | return nil 144 | } 145 | var fileIDsByModule: [String: Set] = [:] 146 | var registriesByFileID: [String: [PreviewRegistryWrapper]] = [:] 147 | for item in sortedPreviewRegistries { 148 | fileIDsByModule[item.module, default: []].insert(item.fileID) 149 | registriesByFileID[item.fileID, default: []].append(item) 150 | } 151 | return fileIDsByModule.keys.sorted().map { module in 152 | return Node.folder( 153 | .init( 154 | title: module, 155 | contents: { [fileIDs = fileIDsByModule[module]!.sorted()] in 156 | fileIDs.map { fileID in 157 | 158 | let name = String(fileID[fileID.index(after: module.endIndex)...]) 159 | let registries = registriesByFileID[fileID]! 160 | 161 | return Node.folder( 162 | .init( 163 | title: name, 164 | contents: { 165 | registries.map { registry in 166 | 167 | let pageName: String 168 | 169 | if let displayName = registry.displayName { 170 | pageName = "\(displayName)" 171 | } else { 172 | pageName = "line: \(registry.line)" 173 | } 174 | 175 | return Node.page( 176 | .init( 177 | fileID, 178 | registry.line, 179 | title: pageName, 180 | usesScrollView: false, 181 | destination: { 182 | AnyView(registry.makeView()) 183 | } 184 | ) 185 | ) 186 | 187 | } 188 | })) 189 | 190 | } 191 | } 192 | ) 193 | ) 194 | } 195 | } 196 | 197 | } 198 | 199 | @resultBuilder 200 | public struct FolderBuilder { 201 | 202 | public typealias Element = Book.Node 203 | 204 | @MainActor 205 | public static func buildExpression(_ expression: Provider.Type) 206 | -> [FolderBuilder.Element] 207 | { 208 | return [.page(expression.bookBody)] 209 | } 210 | 211 | public static func buildExpression(_ expression: BookPage) -> [FolderBuilder.Element] { 212 | return [.page(expression)] 213 | } 214 | 215 | public static func buildExpression(_ expression: [BookPage]) -> [FolderBuilder.Element] { 216 | return expression.map { .page($0) } 217 | } 218 | 219 | public static func buildExpression(_ expression: Book) -> [FolderBuilder.Element] { 220 | return [.folder(expression)] 221 | } 222 | 223 | public static func buildExpression(_ expression: [Book]) -> [FolderBuilder.Element] { 224 | return expression.map { .folder($0) } 225 | } 226 | 227 | public static func buildBlock() -> [Element] { 228 | [] 229 | } 230 | 231 | public static func buildBlock(_ contents: C...) -> [Element] 232 | where C.Element == Element { 233 | return contents.flatMap { $0 } 234 | } 235 | 236 | public static func buildOptional(_ component: [Element]?) -> [Element] { 237 | return component ?? [] 238 | } 239 | 240 | public static func buildEither(first component: [Element]) -> [Element] { 241 | return component 242 | } 243 | 244 | public static func buildEither(second component: [Element]) -> [Element] { 245 | return component 246 | } 247 | 248 | public static func buildArray(_ components: [[Element]]) -> [Element] { 249 | components.flatMap { $0 } 250 | } 251 | 252 | public static func buildExpression(_ element: Element?) -> [Element] { 253 | return element.map { [$0] } ?? [] 254 | } 255 | 256 | public static func buildExpression(_ element: Element) -> [Element] { 257 | return [element] 258 | } 259 | 260 | public static func buildExpression(_ elements: C) -> [Element] 261 | where C.Element == Element { 262 | Array(elements) 263 | } 264 | 265 | public static func buildExpression(_ elements: C) -> [Element] 266 | where C.Element == Element? { 267 | elements.compactMap { $0 } 268 | } 269 | 270 | } 271 | 272 | private struct NodeOutlineGroup: View where Node: Identifiable, Content: View { 273 | 274 | let node: Node 275 | let childKeyPath: KeyPath 276 | 277 | let expandsAll: Bool 278 | 279 | @State var isExpanded: Bool 280 | 281 | let content: (Node) -> Content 282 | 283 | init( 284 | expandsAll: Bool, 285 | _ node: Node, children childKeyPath: KeyPath, 286 | @ViewBuilder content: @escaping (Node) -> Content 287 | ) { 288 | self._isExpanded = .init(wrappedValue: expandsAll) 289 | self.expandsAll = expandsAll 290 | self.node = node 291 | self.childKeyPath = childKeyPath 292 | self.content = content 293 | } 294 | 295 | var body: some View { 296 | Group { 297 | if node[keyPath: childKeyPath] != nil { 298 | DisclosureGroup( 299 | isExpanded: $isExpanded, 300 | content: { 301 | ForEach(node[keyPath: childKeyPath]!) { childNode in 302 | NodeOutlineGroup( 303 | expandsAll: expandsAll, 304 | childNode, 305 | children: childKeyPath, 306 | content: content 307 | ) 308 | } 309 | }, 310 | label: { 311 | content(node) 312 | } 313 | ) 314 | 315 | } else { 316 | content(node) 317 | } 318 | } 319 | .onChange(of: expandsAll) { value in 320 | withAnimation(.default) { 321 | isExpanded = value 322 | } 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/BookAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | public struct BookAction: BookView { 25 | 26 | @Environment(\.storybook_targetViewController) var targetViewController 27 | 28 | public let declarationIdentifier: DeclarationIdentifier 29 | 30 | public let action: @MainActor (UIViewController) -> Void 31 | 32 | public let title: String 33 | 34 | public init( 35 | title: String, 36 | action: @escaping @MainActor (UIViewController) -> Void 37 | ) { 38 | self.title = title 39 | self.action = action 40 | self.declarationIdentifier = .init() 41 | } 42 | 43 | public var body: some View { 44 | Button(title) { 45 | if let targetViewController { 46 | action(targetViewController) 47 | } else { 48 | 49 | } 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/BookPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import SwiftUI 24 | import ResultBuilderKit 25 | 26 | public struct DeclarationIdentifier: Hashable, Codable, Sendable { 27 | 28 | public let index: Int 29 | 30 | nonisolated init() { 31 | index = issueUniqueNumber() 32 | } 33 | 34 | public init(raw index: Int) { 35 | self.index = index 36 | } 37 | } 38 | 39 | private let _lock = NSLock() 40 | private nonisolated(unsafe) var _counter: Int = 0 41 | private func issueUniqueNumber() -> Int { 42 | _lock.lock() 43 | defer { 44 | _lock.unlock() 45 | } 46 | _counter += 1 47 | return _counter 48 | } 49 | 50 | /// A component that displays a disclosure view. 51 | public struct BookPage: BookView, Identifiable { 52 | 53 | @Environment(\.bookContext) var context 54 | 55 | public nonisolated var id: DeclarationIdentifier { 56 | declarationIdentifier 57 | } 58 | 59 | public let usesScrollView: Bool 60 | public let title: String 61 | public let destination: AnyView 62 | public nonisolated let declarationIdentifier: DeclarationIdentifier 63 | private let fileID: any StringProtocol 64 | private let line: any FixedWidthInteger 65 | 66 | public init( 67 | _ fileID: any StringProtocol = #fileID, 68 | _ line: any FixedWidthInteger = #line, 69 | title: String, 70 | usesScrollView: Bool = true, 71 | @ViewBuilder destination: @MainActor () -> Destination 72 | ) { 73 | self.fileID = fileID 74 | self.line = line 75 | self.title = title 76 | self.usesScrollView = usesScrollView 77 | self.destination = AnyView(destination()) 78 | self.declarationIdentifier = .init() 79 | } 80 | 81 | public var body: some View { 82 | 83 | NavigationLink { 84 | Group { 85 | if usesScrollView { 86 | ScrollView { 87 | destination 88 | } 89 | } else { 90 | destination 91 | } 92 | } 93 | .listStyle(.plain) 94 | .navigationTitle(title) 95 | .navigationBarTitleDisplayMode(.inline) 96 | .onAppear(perform: { 97 | context?.onOpen(pageID: id) 98 | }) 99 | } label: { 100 | LinkLabel(title: title, fileID: fileID, line: line) 101 | .contextMenu(menuItems: { 102 | Text(title) 103 | }) { 104 | destination 105 | } 106 | } 107 | 108 | } 109 | } 110 | 111 | private struct LinkLabel: View { 112 | 113 | let title: any StringProtocol 114 | let fileID: any StringProtocol 115 | let line: any FixedWidthInteger 116 | 117 | var body: some View { 118 | HStack { 119 | Image.init(systemName: "doc") 120 | VStack(alignment: .leading) { 121 | Text(title) 122 | Text("\(fileID):\(line)") 123 | .font(.caption.monospacedDigit()) 124 | .opacity(0.8) 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/BookPresent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | /// A component descriptor that just displays UI-Component 25 | public struct BookPresent: BookView { 26 | 27 | @Environment(\.storybook_targetViewController) private var targetViewController 28 | 29 | public let declarationIdentifier: DeclarationIdentifier 30 | public let presentedViewControllerBlock: @MainActor () -> UIViewController 31 | 32 | public let title: String 33 | 34 | public init( 35 | title: String, 36 | presentingViewControllerBlock: @escaping @MainActor () -> UIViewController 37 | ) { 38 | self.title = title 39 | self.presentedViewControllerBlock = presentingViewControllerBlock 40 | self.declarationIdentifier = .init() 41 | } 42 | 43 | public var body: some View { 44 | Button(title) { 45 | let viewController = presentedViewControllerBlock() 46 | targetViewController?.present(viewController, animated: true) 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/BookPreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | private struct FrameConstraint { 25 | var minWidth: CGFloat? = nil 26 | var idealWidth: CGFloat? = nil 27 | var maxWidth: CGFloat? = nil 28 | var minHeight: CGFloat? = nil 29 | var idealHeight: CGFloat? = nil 30 | var maxHeight: CGFloat? = nil 31 | } 32 | 33 | public struct BookPreview: BookView { 34 | 35 | public struct Context { 36 | 37 | var controlView: AnyView? 38 | 39 | public mutating func control(_ content: () -> Content) { 40 | controlView = AnyView(content()) 41 | } 42 | 43 | } 44 | 45 | public let viewBlock: @MainActor (inout Context) -> UIView 46 | 47 | public let declarationIdentifier: DeclarationIdentifier 48 | 49 | private let fileID: any StringProtocol 50 | private let line: any FixedWidthInteger 51 | private let title: String? 52 | private var frameConstraint: FrameConstraint = .init() 53 | 54 | public init( 55 | _ fileID: any StringProtocol = #fileID, 56 | _ line: any FixedWidthInteger = #line, 57 | title: String? = nil, 58 | viewBlock: @escaping @MainActor (inout Context) -> UIView 59 | ) { 60 | 61 | self.title = title 62 | self.fileID = fileID 63 | self.line = line 64 | self.viewBlock = viewBlock 65 | 66 | self.declarationIdentifier = .init() 67 | } 68 | 69 | @State var controlView: AnyView? 70 | 71 | public var body: some View { 72 | 73 | VStack { 74 | if let title { 75 | Text(title) 76 | .font(.system(size: 17, weight: .semibold)) 77 | } 78 | 79 | _ViewHost( 80 | maxWidth: UIScreen.main.bounds.size.width /* so bad but it's for sizing with autolayout */, 81 | instantiate: { 82 | 83 | var context: Context = .init() 84 | let view = viewBlock(&context) 85 | 86 | // TODO: currently using workaround 87 | Task { 88 | controlView = context.controlView 89 | } 90 | 91 | return _View(element: view, frameConstraint: frameConstraint) 92 | }, 93 | update: { _, _ in } 94 | ) 95 | 96 | controlView 97 | 98 | Text("\(fileID):\(line)") 99 | .font(.caption.monospacedDigit()) 100 | 101 | BookSpacer(height: 16) 102 | 103 | } 104 | 105 | } 106 | 107 | public func previewFrame( 108 | width: CGFloat?, 109 | height: CGFloat? 110 | ) -> Self { 111 | modified { 112 | $0.frameConstraint.idealWidth = width 113 | $0.frameConstraint.idealHeight = height 114 | } 115 | } 116 | 117 | public func previewFrame( 118 | minWidth: CGFloat? = nil, 119 | idealWidth: CGFloat? = nil, 120 | maxWidth: CGFloat? = nil, 121 | minHeight: CGFloat? = nil, 122 | idealHeight: CGFloat? = nil, 123 | maxHeight: CGFloat? = nil 124 | ) -> Self { 125 | modified { 126 | $0.frameConstraint.minWidth = minWidth 127 | $0.frameConstraint.maxWidth = maxWidth 128 | $0.frameConstraint.minHeight = minHeight 129 | $0.frameConstraint.maxHeight = maxHeight 130 | 131 | $0.frameConstraint.idealWidth = idealWidth 132 | $0.frameConstraint.idealHeight = idealHeight 133 | } 134 | } 135 | 136 | } 137 | 138 | private final class _View: UIView { 139 | 140 | override class var requiresConstraintBasedLayout: Bool { true } 141 | 142 | init() { 143 | super.init(frame: .zero) 144 | 145 | } 146 | 147 | @available(*, unavailable) 148 | required init?( 149 | coder aDecoder: NSCoder 150 | ) { 151 | fatalError("init(coder:) has not been implemented") 152 | } 153 | 154 | convenience init( 155 | element: UIView, 156 | frameConstraint: FrameConstraint 157 | ) { 158 | 159 | self.init() 160 | 161 | element.translatesAutoresizingMaskIntoConstraints = false 162 | element.setContentHuggingPriority(.defaultLow, for: .horizontal) 163 | element.setContentHuggingPriority(.defaultLow, for: .vertical) 164 | 165 | addSubview(element) 166 | 167 | var constraints: [NSLayoutConstraint] = [] 168 | 169 | if let maxWidth = frameConstraint.maxWidth { 170 | if (maxWidth == .infinity) || (maxWidth == .greatestFiniteMagnitude) { 171 | let c = element.widthAnchor.constraint(equalToConstant: 5000) 172 | c.priority = .defaultHigh + 1 173 | constraints.append( 174 | c 175 | ) 176 | } else { 177 | constraints.append( 178 | element.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth) 179 | ) 180 | } 181 | } 182 | 183 | if let maxHeight = frameConstraint.minHeight { 184 | 185 | if (maxHeight == .infinity) || (maxHeight == .greatestFiniteMagnitude) { 186 | let c = element.heightAnchor.constraint(equalToConstant: 5000) 187 | c.priority = .defaultHigh + 1 188 | constraints.append( 189 | c 190 | ) 191 | } else { 192 | 193 | constraints.append( 194 | element.heightAnchor.constraint(lessThanOrEqualToConstant: maxHeight) 195 | ) 196 | } 197 | } 198 | 199 | if let minWidth = frameConstraint.minWidth { 200 | constraints.append( 201 | element.widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth) 202 | ) 203 | } 204 | 205 | if let minHeight = frameConstraint.minHeight { 206 | 207 | constraints.append( 208 | element.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight) 209 | ) 210 | } 211 | 212 | if let idealWidth = frameConstraint.idealWidth { 213 | constraints.append( 214 | element.widthAnchor.constraint(equalToConstant: idealWidth) 215 | ) 216 | } 217 | 218 | if let idealHeight = frameConstraint.idealHeight { 219 | constraints.append( 220 | element.heightAnchor.constraint(equalToConstant: idealHeight) 221 | ) 222 | } 223 | 224 | constraints.append(contentsOf: [ 225 | 226 | element.centerXAnchor.constraint(equalTo: centerXAnchor), 227 | element.centerYAnchor.constraint(equalTo: centerYAnchor), 228 | 229 | element.topAnchor.constraint(greaterThanOrEqualTo: topAnchor), 230 | element.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor), 231 | element.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor), 232 | element.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor), 233 | 234 | ]) 235 | 236 | NSLayoutConstraint.activate(constraints) 237 | 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/BookProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import ResultBuilderKit 23 | import SwiftUI 24 | 25 | @_alwaysEmitConformanceMetadata 26 | public protocol BookProvider { 27 | @MainActor 28 | static var bookBody: BookPage { get } 29 | } 30 | 31 | extension EnvironmentValues { 32 | 33 | @Entry public var bookContext: BookStore? 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/BookPush.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | /// A component descriptor that previewing with push presentation. 25 | public struct BookPush: BookView { 26 | 27 | @Environment(\.storybook_targetViewController) var targetViewController 28 | 29 | public let pushingViewControllerBlock: @MainActor () -> UIViewController 30 | public let declarationIdentifier: DeclarationIdentifier 31 | 32 | public let title: String 33 | 34 | public init( 35 | title: String, 36 | pushingViewControllerBlock: @escaping @MainActor () -> UIViewController 37 | ) { 38 | self.title = title 39 | self.pushingViewControllerBlock = pushingViewControllerBlock 40 | self.declarationIdentifier = .init() 41 | } 42 | 43 | public var body: some View { 44 | 45 | NavigationLink(title, destination: { 46 | _ViewControllerHost(instantiate: pushingViewControllerBlock) 47 | .environment(\.storybook_targetViewController, targetViewController) 48 | }) 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/BookStore.swift: -------------------------------------------------------------------------------- 1 | 2 | @MainActor 3 | public final class BookStore: ObservableObject { 4 | 5 | @Published var historyPages: [BookPage] = [] 6 | 7 | public let title: String 8 | public let book: Book 9 | 10 | private let allPages: [BookPage.ID: BookPage] 11 | 12 | private let userDefaults = UserDefaults(suiteName: "jp.eure.storybook2") ?? .standard 13 | 14 | public init( 15 | book: Book 16 | ) { 17 | 18 | self.title = book.title 19 | self.book = book 20 | 21 | self.allPages = book.allPages().reduce( 22 | into: [BookPage.ID: BookPage](), 23 | { partialResult, item in 24 | partialResult[item.id] = item 25 | } 26 | ) 27 | 28 | updateHistory() 29 | } 30 | 31 | private func updateHistory() { 32 | 33 | let indexes = userDefaults.array(forKey: "history") as? [Int] ?? [] 34 | 35 | let _pages = indexes.compactMap { (index: Int) -> BookPage? in 36 | let id = DeclarationIdentifier(raw: index) 37 | guard let page = allPages[id] else { 38 | return nil 39 | } 40 | return page 41 | } 42 | 43 | historyPages = _pages 44 | 45 | } 46 | 47 | func onOpen(pageID: DeclarationIdentifier) { 48 | 49 | guard allPages.keys.contains(pageID) else { 50 | return 51 | } 52 | 53 | let index = pageID.index 54 | 55 | var current = userDefaults.array(forKey: "history") as? [Int] ?? [] 56 | if let index = current.firstIndex(of: index) { 57 | current.remove(at: index) 58 | } 59 | 60 | current.insert(index, at: 0) 61 | current = Array(current.prefix(5)) 62 | 63 | userDefaults.set(current, forKey: "history") 64 | 65 | print("Update history", current) 66 | 67 | updateHistory() 68 | } 69 | 70 | nonisolated func search(query: String) async -> [BookPage] { 71 | 72 | // find pages using query but fuzzy 73 | let pages = allPages.values 74 | .map { page -> (score: Double, page: BookPage) in 75 | let score = page.title.score(word: query) 76 | return (score, page) 77 | } 78 | .filter { $0.score > 0 } 79 | .sorted { $0.score > $1.score } 80 | .map { $0.page } 81 | 82 | return pages 83 | } 84 | 85 | } 86 | 87 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/BookText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | public struct BookText: View { 25 | 26 | public let text: String 27 | public var foregroundColor: UIColor = { 28 | if #available(iOS 13.0, *) { 29 | return .label 30 | } else { 31 | return .darkText 32 | } 33 | }() 34 | 35 | public var font: UIFont = .preferredFont(forTextStyle: .body) 36 | 37 | public init(_ text: String) { 38 | self.text = text 39 | } 40 | 41 | public var body: some View { 42 | Text(text) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Primitives/BookView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import UIKit 23 | import SwiftUI 24 | 25 | public protocol BookView: SwiftUI.View { 26 | 27 | } 28 | 29 | extension BookView { 30 | 31 | public func modified(_ modify: (inout Self) -> Void) -> Self { 32 | var s = self 33 | modify(&s) 34 | return s 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Sources/StorybookKit/SearchBar.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SearchBar: View { 4 | 5 | @Binding var text: String 6 | 7 | var body: some View { 8 | 9 | ZStack { 10 | 11 | RoundedRectangle(cornerRadius: 8) 12 | .fill(Color(red: 239 / 255, 13 | green: 239 / 255, 14 | blue: 241 / 255)) 15 | .frame(height: 36) 16 | 17 | HStack(spacing: 6) { 18 | 19 | Image(systemName: "magnifyingglass") 20 | .foregroundColor(.gray) 21 | 22 | if #available(iOS 15, *) { 23 | TextField("Search", text: $text) 24 | .autocorrectionDisabled() 25 | .textInputAutocapitalization(.never) 26 | } else { 27 | TextField("Search", text: $text) 28 | .autocorrectionDisabled() 29 | } 30 | 31 | if !text.isEmpty { 32 | Button { 33 | text.removeAll() 34 | } label: { 35 | Image(systemName: "xmark.circle.fill") 36 | .foregroundColor(.gray) 37 | } 38 | .padding(.trailing, 6) 39 | } 40 | } 41 | .padding() 42 | } 43 | 44 | } 45 | } 46 | 47 | #if DEBUG 48 | 49 | enum Preview_SearchBar: PreviewProvider { 50 | 51 | static var previews: some View { 52 | 53 | Group { 54 | SearchBar(text: .constant("A")) 55 | } 56 | 57 | } 58 | 59 | } 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /Sources/StorybookKit/Storybook.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @available(iOS 17.0, *) 4 | public struct Storybook: View { 5 | 6 | public init() { 7 | 8 | } 9 | 10 | public var body: some View { 11 | StorybookDisplayRootView( 12 | bookStore: .init( 13 | book: Book.init(title: "Contents") { 14 | 15 | if let nodes = Book.allBookPreviews() { 16 | Book(title: "#Preview") { 17 | nodes 18 | } 19 | } 20 | 21 | } 22 | ) 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/StorybookKit/StorybookDisplayRootView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftUISupport 3 | import UIKit 4 | 5 | public struct StorybookDisplayRootView: View { 6 | 7 | let book: BookContainer 8 | 9 | @MainActor 10 | public init(bookStore: BookStore) { 11 | self.book = .init(store: bookStore) 12 | } 13 | 14 | public var body: some View { 15 | 16 | _ViewControllerHost { 17 | let controller = _ViewController(content: book) 18 | return controller 19 | } 20 | .ignoresSafeArea() 21 | 22 | } 23 | } 24 | 25 | public struct BookActionHosting: View { 26 | 27 | private let content: Content 28 | 29 | public init(@ViewBuilder content: () -> Content) { 30 | self.content = content() 31 | } 32 | 33 | public var body: some View { 34 | 35 | _ViewControllerHost { 36 | let controller = _ViewController(content: content) 37 | return controller 38 | } 39 | .ignoresSafeArea() 40 | 41 | } 42 | } 43 | 44 | struct BookContainer: View { 45 | 46 | struct UniqueBox: Hashable { 47 | 48 | static func == (lhs: UniqueBox, rhs: UniqueBox) -> Bool { 49 | lhs.uuid == rhs.uuid 50 | } 51 | 52 | func hash(into hasher: inout Hasher) { 53 | hasher.combine(uuid) 54 | } 55 | 56 | let uuid: UUID = .init() 57 | let value: T 58 | 59 | init(value: T) { 60 | self.value = value 61 | } 62 | } 63 | 64 | // MARK: Properties 65 | 66 | @ObservedObject private var store: BookStore 67 | 68 | @State private var lastUsedItem: UniqueBox? 69 | @State private var query: String = "" 70 | @State private var result: [BookPage] = [] 71 | @State private var currentTask: Task? 72 | 73 | @State var path: NavigationPath = .init() 74 | 75 | // MARK: Initializers 76 | 77 | @MainActor 78 | public init( 79 | store: BookStore 80 | ) { 81 | self.store = store 82 | } 83 | 84 | // MARK: View 85 | 86 | public var body: some View { 87 | 88 | NavigationStack(path: $path) { 89 | List { 90 | 91 | if result.isEmpty == false { 92 | Section { 93 | ForEach(result) { link in 94 | link 95 | } 96 | } header: { 97 | Text("Search Result") 98 | } 99 | } 100 | 101 | Section { 102 | ForEach(store.historyPages) { link in 103 | link 104 | } 105 | } header: { 106 | Text("History") 107 | } 108 | 109 | store.book 110 | 111 | } 112 | .navigationTitle(store.title) 113 | .searchable(text: $query, prompt: "Search") 114 | .navigationDestination(for: UniqueBox.self) { page in 115 | page 116 | .value 117 | .destination 118 | .environment(\.bookContext, store) 119 | } 120 | } 121 | 122 | .tabItem { 123 | Image(systemName: "list.bullet") 124 | Text("List") 125 | } 126 | .environment(\.bookContext, store) 127 | .onAppear { 128 | if let value = store.historyPages.first { 129 | path.append(UniqueBox(value: value)) 130 | } 131 | } 132 | .onChange(of: query, perform: { value in 133 | 134 | guard value.isEmpty == false else { 135 | currentTask?.cancel() 136 | result = [] 137 | return 138 | } 139 | 140 | currentTask?.cancel() 141 | currentTask = Task { 142 | 143 | let result = await store.search(query: value) 144 | 145 | print(result.map { $0.title }) 146 | 147 | guard Task.isCancelled == false else { 148 | return 149 | } 150 | 151 | self.result = result 152 | } 153 | 154 | }) 155 | } 156 | 157 | } 158 | 159 | final class _ViewController: UIViewController { 160 | 161 | private let content: Content 162 | 163 | init(content: Content) { 164 | self.content = content 165 | super.init(nibName: nil, bundle: nil) 166 | } 167 | 168 | required init?(coder: NSCoder) { 169 | fatalError("init(coder:) has not been implemented") 170 | } 171 | 172 | override func viewDidLoad() { 173 | super.viewDidLoad() 174 | 175 | let hosting = UIHostingController( 176 | rootView: 177 | content 178 | .environment(\.storybook_targetViewController, self) 179 | ) 180 | 181 | addChild(hosting) 182 | view.addSubview(hosting.view) 183 | hosting.view.translatesAutoresizingMaskIntoConstraints = false 184 | 185 | NSLayoutConstraint.activate([ 186 | hosting.view.topAnchor.constraint(equalTo: view.topAnchor), 187 | hosting.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), 188 | hosting.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 189 | hosting.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), 190 | ]) 191 | 192 | hosting.didMove(toParent: self) 193 | 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /Sources/StorybookKit/StorybookKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | 24 | @_exported import SwiftUI 25 | 26 | @freestanding(declaration) 27 | public macro StorybookPage( 28 | title: String, 29 | @ViewBuilder contents: @escaping @MainActor () -> any View 30 | ) = #externalMacro( 31 | module: "StorybookMacrosPlugin", 32 | type: "StorybookPageMacro" 33 | ) 34 | 35 | @freestanding(declaration) 36 | public macro StorybookPage( 37 | target: Target.Type = Target.self, 38 | @ViewBuilder contents: @escaping @MainActor () -> any View 39 | ) = #externalMacro( 40 | module: "StorybookMacrosPlugin", 41 | type: "StorybookPageMacro" 42 | ) 43 | 44 | @freestanding(expression) 45 | public macro StorybookPreview( 46 | title: String, 47 | @ViewBuilder contents: @escaping @MainActor () -> any View 48 | ) -> AnyView = #externalMacro( 49 | module: "StorybookMacrosPlugin", 50 | type: "StorybookPreviewMacro" 51 | ) 52 | 53 | @freestanding(expression) 54 | public macro StorybookPreview( 55 | target: Target.Type = Target.self, 56 | @ViewBuilder contents: @escaping @MainActor () -> any View 57 | ) -> AnyView = #externalMacro( 58 | module: "StorybookMacrosPlugin", 59 | type: "StorybookPreviewMacro" 60 | ) 61 | -------------------------------------------------------------------------------- /Sources/StorybookKitTextureSupport/BookNodePreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import AsyncDisplayKit 23 | import SwiftUI 24 | import StorybookKit 25 | import TextureBridging 26 | import TextureSwiftSupport 27 | 28 | public struct BookNodePreview: BookView { 29 | 30 | private var backing: BookPreview 31 | 32 | public init( 33 | _ fileID: any StringProtocol = #fileID, 34 | _ line: any FixedWidthInteger = #line, 35 | title: String? = nil, 36 | nodeBlock: @escaping @MainActor (inout BookPreview.Context) -> ASDisplayNode 37 | ) { 38 | 39 | self.backing = .init(fileID, line, title: title) { context in 40 | let body = nodeBlock(&context) 41 | 42 | let node = AnyDisplayNode { _, size in 43 | return LayoutSpec { 44 | VStackLayout { body.flexGrow(1).flexShrink(1) } 45 | } 46 | } 47 | return NodeView(node: node) 48 | } 49 | 50 | } 51 | 52 | public var body: some View { 53 | backing 54 | } 55 | 56 | public func previewFrame( 57 | width: CGFloat?, 58 | height: CGFloat? 59 | ) -> Self { 60 | modified { 61 | $0.backing = $0.backing.previewFrame(width: width, height: height) 62 | } 63 | } 64 | 65 | public func previewFrame( 66 | minWidth: CGFloat? = nil, 67 | idealWidth: CGFloat? = nil, 68 | maxWidth: CGFloat? = nil, 69 | minHeight: CGFloat? = nil, 70 | idealHeight: CGFloat? = nil, 71 | maxHeight: CGFloat? = nil 72 | ) -> Self { 73 | modified { 74 | $0.backing = $0.backing.previewFrame( 75 | minWidth: minWidth, 76 | idealWidth: idealWidth, 77 | maxWidth: maxWidth, 78 | minHeight: minHeight, 79 | idealHeight: idealHeight, 80 | maxHeight: maxHeight 81 | ) 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/StorybookKitTextureSupport/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/StorybookKitTextureSupport/StorybookKitTextureSupport.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | #import 23 | 24 | //! Project version number for StorybookKitTextureSupport. 25 | FOUNDATION_EXPORT double StorybookKitTextureSupportVersionNumber; 26 | 27 | //! Project version string for StorybookKitTextureSupport. 28 | FOUNDATION_EXPORT const unsigned char StorybookKitTextureSupportVersionString[]; 29 | 30 | // In this header, you should import all the public headers of your framework using statements like #import 31 | 32 | 33 | -------------------------------------------------------------------------------- /Sources/StorybookMacrosPlugin/StorybookMacrosPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftCompilerPlugin 23 | import SwiftSyntaxMacros 24 | 25 | // MARK: - StorybookMacrosPlugin 26 | 27 | @main 28 | struct StorybookMacrosPlugin: CompilerPlugin { 29 | 30 | // MARK: CompilerPlugin 31 | 32 | let providingMacros: [Macro.Type] = [ 33 | StorybookPageMacro.self, 34 | StorybookPreviewMacro.self 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /Sources/StorybookMacrosPlugin/StorybookPageMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import SwiftCompilerPlugin 24 | import SwiftSyntax 25 | import SwiftSyntaxBuilder 26 | import SwiftSyntaxMacros 27 | 28 | // MARK: - StorybookPageMacro 29 | 30 | public struct StorybookPageMacro: DeclarationMacro { 31 | 32 | // MARK: Internal 33 | 34 | /// Should match `Book._magicSubstring` 35 | static let _magicSubstring: String = "__🤖🛠️_StorybookMagic_" 36 | 37 | // MARK: DeclarationMacro 38 | 39 | public static func expansion( 40 | of node: some FreestandingMacroExpansionSyntax, 41 | in context: some MacroExpansionContext 42 | ) throws -> [DeclSyntax] { 43 | let (title, closure) = try self.parseArguments(from: node) 44 | let enumName = context.makeUniqueName( 45 | self._magicSubstring 46 | ) 47 | return [ 48 | .init( 49 | stringLiteral: """ 50 | enum \(enumName): BookProvider { 51 | @MainActor 52 | static var bookBody: BookPage { 53 | .init( 54 | title: \(title), 55 | destination: \(closure) 56 | ) 57 | } 58 | } 59 | """ 60 | ) 61 | ] 62 | } 63 | 64 | 65 | // MARK: Private 66 | 67 | private static func parseArguments( 68 | from node: some FreestandingMacroExpansionSyntax 69 | ) throws -> ( 70 | title: ExprSyntax, 71 | closure: ClosureExprSyntax 72 | ) { 73 | var argumentsIterator = node.argumentList.makeIterator() 74 | var title: ExprSyntax? = (node.genericArgumentClause?.arguments.first?.argument) 75 | .flatMap { genericType -> TypeSyntaxProtocol? in 76 | genericType.as(IdentifierTypeSyntax.self) 77 | ?? genericType.as(MemberTypeSyntax.self) 78 | } 79 | .map({ .init(stringLiteral: "_typeName(\($0).self)") }) 80 | 81 | var closure: ClosureExprSyntax? = node.trailingClosure 82 | while let argument = argumentsIterator.next()? 83 | .as(LabeledExprSyntax.self) { 84 | 85 | switch argument.label?.text { 86 | 87 | case "title"?: 88 | title = argument.expression 89 | 90 | case "target"?: 91 | title = .init(stringLiteral: "_typeName(\(argument))") 92 | 93 | case "contents"?: 94 | closure = argument.expression 95 | .as(ClosureExprSyntax.self) 96 | 97 | default: 98 | fatalError() 99 | } 100 | } 101 | return ( 102 | title: title!, 103 | closure: closure! 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/StorybookMacrosPlugin/StorybookPreviewMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import SwiftCompilerPlugin 24 | import SwiftSyntax 25 | import SwiftSyntaxBuilder 26 | import SwiftSyntaxMacros 27 | 28 | // MARK: - StorybookPreviewMacro 29 | 30 | public struct StorybookPreviewMacro: ExpressionMacro { 31 | 32 | // MARK: Internal 33 | 34 | /// Should match `Book._magicSubstring` 35 | static let _magicSubstring: String = "__🤖🛠️_StorybookMagic_" 36 | 37 | // MARK: ExpressionMacro 38 | 39 | public static func expansion( 40 | of node: some FreestandingMacroExpansionSyntax, 41 | in context: some MacroExpansionContext 42 | ) throws -> ExprSyntax { 43 | let (title, closure) = try self.parseArguments(from: node) 44 | let enumName = context.makeUniqueName( 45 | self._magicSubstring 46 | ) 47 | return .init( 48 | stringLiteral: """ 49 | { 50 | enum \(enumName): BookProvider { 51 | @MainActor 52 | static var bookBody: BookPage { 53 | .init( 54 | title: \(title), 55 | destination: \(closure) 56 | ) 57 | } 58 | } 59 | return \(enumName).bookBody.destination 60 | }() 61 | """ 62 | ) 63 | } 64 | 65 | 66 | // MARK: Private 67 | 68 | private static func parseArguments( 69 | from node: some FreestandingMacroExpansionSyntax 70 | ) throws -> ( 71 | title: ExprSyntax, 72 | closure: ClosureExprSyntax 73 | ) { 74 | var argumentsIterator = node.argumentList.makeIterator() 75 | var title: ExprSyntax? = (node.genericArgumentClause?.arguments.first?.argument) 76 | .flatMap { genericType -> TypeSyntaxProtocol? in 77 | genericType.as(IdentifierTypeSyntax.self) 78 | ?? genericType.as(MemberTypeSyntax.self) 79 | } 80 | .map({ .init(stringLiteral: "_typeName(\($0).self)") }) 81 | 82 | var closure: ClosureExprSyntax? = node.trailingClosure 83 | while let argument = argumentsIterator.next()? 84 | .as(LabeledExprSyntax.self) { 85 | 86 | switch argument.label?.text { 87 | 88 | case "title"?: 89 | title = argument.expression 90 | 91 | case "target"?: 92 | title = .init(stringLiteral: "_typeName(\(argument))") 93 | 94 | case "contents"?: 95 | closure = argument.expression 96 | .as(ClosureExprSyntax.self) 97 | 98 | default: 99 | fatalError() 100 | } 101 | } 102 | return ( 103 | title: title!, 104 | closure: closure! 105 | ) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/StorybookMacrosTests/StorybookPageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import MacroTesting 23 | import XCTest 24 | import StorybookMacrosPlugin 25 | 26 | final class StorybookPageTests: XCTestCase { 27 | 28 | override func invokeTest() { 29 | withMacroTesting( 30 | isRecording: false, 31 | macros: ["StorybookPage": StorybookPageMacro.self] 32 | ) { 33 | super.invokeTest() 34 | } 35 | } 36 | 37 | func testUnlabeledTitleUnlabeledContent() { 38 | assertMacro { 39 | """ 40 | #StorybookPage { 41 | VStack { 42 | Text("test") 43 | } 44 | .tint(.accentColor) 45 | } 46 | """ 47 | } expansion: { 48 | """ 49 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 50 | @MainActor 51 | static var bookBody: BookPage { 52 | .init( 53 | title: _typeName(UIView.self), 54 | destination: { 55 | VStack { 56 | Text("test") 57 | } 58 | .tint(.accentColor) 59 | } 60 | ) 61 | } 62 | } 63 | """ 64 | } 65 | 66 | assertMacro { 67 | """ 68 | #StorybookPage(title: "Path1.Path2.Title") { 69 | VStack { 70 | Text("test") 71 | } 72 | .tint(.accentColor) 73 | } 74 | """ 75 | } expansion: { 76 | """ 77 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 78 | @MainActor 79 | static var bookBody: BookPage { 80 | .init( 81 | title: "Path1.Path2.Title", 82 | destination: { 83 | VStack { 84 | Text("test") 85 | } 86 | .tint(.accentColor) 87 | } 88 | ) 89 | } 90 | } 91 | """ 92 | } 93 | } 94 | 95 | func testLabeledTitleUnlabeledContent() { 96 | assertMacro { 97 | """ 98 | enum Namespace1 { enum Namespace2 { enum Namespace3 { class TestableView: UIView {} } } } 99 | #StorybookPage(target: Namespace1.Namespace2.Namespace3.TestableView.self) { 100 | VStack { 101 | Text("test") 102 | } 103 | .tint(.accentColor) 104 | } 105 | """ 106 | } expansion: { 107 | """ 108 | enum Namespace1 { enum Namespace2 { enum Namespace3 { class TestableView: UIView {} } } } 109 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 110 | @MainActor 111 | static var bookBody: BookPage { 112 | .init( 113 | title: _typeName(target: Namespace1.Namespace2.Namespace3.TestableView.self), 114 | destination: { 115 | VStack { 116 | Text("test") 117 | } 118 | .tint(.accentColor) 119 | } 120 | ) 121 | } 122 | } 123 | """ 124 | } 125 | 126 | assertMacro { 127 | """ 128 | #StorybookPage(title: "Path1.Path2.Title") { 129 | VStack { 130 | Text("test") 131 | } 132 | .tint(.accentColor) 133 | } 134 | """ 135 | } expansion: { 136 | """ 137 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 138 | @MainActor 139 | static var bookBody: BookPage { 140 | .init( 141 | title: "Path1.Path2.Title", 142 | destination: { 143 | VStack { 144 | Text("test") 145 | } 146 | .tint(.accentColor) 147 | } 148 | ) 149 | } 150 | } 151 | """ 152 | } 153 | } 154 | 155 | func testUnlabeledTitleLabeledContent() { 156 | assertMacro { 157 | """ 158 | #StorybookPage( 159 | contents: { 160 | VStack { 161 | Text("test") 162 | } 163 | .tint(.accentColor) 164 | } 165 | ) 166 | """ 167 | } expansion: { 168 | """ 169 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 170 | @MainActor 171 | static var bookBody: BookPage { 172 | .init( 173 | title: _typeName(UIView.self), 174 | destination: { 175 | VStack { 176 | Text("test") 177 | } 178 | .tint(.accentColor) 179 | } 180 | ) 181 | } 182 | } 183 | """ 184 | } 185 | 186 | assertMacro { 187 | """ 188 | #StorybookPage( 189 | title: "Path1.Path2.Title", 190 | contents: { 191 | VStack { 192 | Text("test") 193 | } 194 | .tint(.accentColor) 195 | } 196 | ) 197 | """ 198 | } expansion: { 199 | """ 200 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 201 | @MainActor 202 | static var bookBody: BookPage { 203 | .init( 204 | title: "Path1.Path2.Title", 205 | destination: { 206 | VStack { 207 | Text("test") 208 | } 209 | .tint(.accentColor) 210 | } 211 | ) 212 | } 213 | } 214 | """ 215 | } 216 | } 217 | 218 | func testLabeledTitleLabeledContent() { 219 | assertMacro { 220 | """ 221 | enum Namespace1 { enum Namespace2 { enum Namespace3 { class TestableView: UIView {} } } } 222 | #StorybookPage( 223 | target: Namespace1.Namespace2.Namespace3.TestableView.self, 224 | contents: { 225 | VStack { 226 | Text("test") 227 | } 228 | .tint(.accentColor) 229 | } 230 | ) 231 | """ 232 | } expansion: { 233 | """ 234 | enum Namespace1 { enum Namespace2 { enum Namespace3 { class TestableView: UIView {} } } } 235 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 236 | @MainActor 237 | static var bookBody: BookPage { 238 | .init( 239 | title: _typeName( 240 | target: Namespace1.Namespace2.Namespace3.TestableView.self,), 241 | destination: { 242 | VStack { 243 | Text("test") 244 | } 245 | .tint(.accentColor) 246 | } 247 | ) 248 | } 249 | } 250 | """ 251 | } 252 | 253 | assertMacro { 254 | """ 255 | #StorybookPage( 256 | title: "Path1.Path2.Title", 257 | contents: { 258 | VStack { 259 | Text("test") 260 | } 261 | .tint(.accentColor) 262 | } 263 | ) 264 | """ 265 | } expansion: { 266 | """ 267 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 268 | @MainActor 269 | static var bookBody: BookPage { 270 | .init( 271 | title: "Path1.Path2.Title", 272 | destination: { 273 | VStack { 274 | Text("test") 275 | } 276 | .tint(.accentColor) 277 | } 278 | ) 279 | } 280 | } 281 | """ 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /Sources/StorybookMacrosTests/StorybookPreviewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import MacroTesting 23 | import XCTest 24 | import StorybookMacrosPlugin 25 | 26 | final class StorybookPreviewTests: XCTestCase { 27 | 28 | override func invokeTest() { 29 | withMacroTesting( 30 | isRecording: false, 31 | macros: ["StorybookPreview": StorybookPreviewMacro.self] 32 | ) { 33 | super.invokeTest() 34 | } 35 | } 36 | 37 | func testUnlabeledTitleUnlabeledContent() { 38 | assertMacro { 39 | """ 40 | #Preview("Some title") { 41 | #StorybookPreview { 42 | VStack { 43 | Text("test") 44 | } 45 | .tint(.accentColor) 46 | } 47 | } 48 | """ 49 | } expansion: { 50 | """ 51 | #Preview("Some title") { 52 | { 53 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 54 | @MainActor 55 | static var bookBody: BookPage { 56 | .init( 57 | title: _typeName(UIView.self), 58 | destination: { 59 | VStack { 60 | Text("test") 61 | } 62 | .tint(.accentColor) 63 | } 64 | ) 65 | } 66 | } 67 | return __macro_local_20__🤖🛠️_StorybookMagic_fMu_.bookBody.destination 68 | }() 69 | } 70 | """ 71 | } 72 | 73 | assertMacro { 74 | """ 75 | #Preview { 76 | #StorybookPreview(title: "Path1.Path2.Title") { 77 | VStack { 78 | Text("test") 79 | } 80 | .tint(.accentColor) 81 | } 82 | } 83 | """ 84 | } expansion: { 85 | """ 86 | #Preview { 87 | { 88 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 89 | @MainActor 90 | static var bookBody: BookPage { 91 | .init( 92 | title: "Path1.Path2.Title", 93 | destination: { 94 | VStack { 95 | Text("test") 96 | } 97 | .tint(.accentColor) 98 | } 99 | ) 100 | } 101 | } 102 | return __macro_local_20__🤖🛠️_StorybookMagic_fMu_.bookBody.destination 103 | }() 104 | } 105 | """ 106 | } 107 | } 108 | 109 | func testLabeledTitleUnlabeledContent() { 110 | assertMacro { 111 | """ 112 | enum Namespace1 { enum Namespace2 { enum Namespace3 { class TestableView: UIView {} } } } 113 | #Preview { 114 | #StorybookPreview(target: Namespace1.Namespace2.Namespace3.TestableView.self) { 115 | VStack { 116 | Text("test") 117 | } 118 | .tint(.accentColor) 119 | } 120 | } 121 | """ 122 | } expansion: { 123 | """ 124 | enum Namespace1 { enum Namespace2 { enum Namespace3 { class TestableView: UIView {} } } } 125 | #Preview { 126 | { 127 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 128 | @MainActor 129 | static var bookBody: BookPage { 130 | .init( 131 | title: _typeName(target: Namespace1.Namespace2.Namespace3.TestableView.self), 132 | destination: { 133 | VStack { 134 | Text("test") 135 | } 136 | .tint(.accentColor) 137 | } 138 | ) 139 | } 140 | } 141 | return __macro_local_20__🤖🛠️_StorybookMagic_fMu_.bookBody.destination 142 | }() 143 | } 144 | """ 145 | } 146 | 147 | assertMacro { 148 | """ 149 | #Preview { 150 | #StorybookPreview(title: "Path1.Path2.Title") { 151 | VStack { 152 | Text("test") 153 | } 154 | .tint(.accentColor) 155 | } 156 | } 157 | """ 158 | } expansion: { 159 | """ 160 | #Preview { 161 | { 162 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 163 | @MainActor 164 | static var bookBody: BookPage { 165 | .init( 166 | title: "Path1.Path2.Title", 167 | destination: { 168 | VStack { 169 | Text("test") 170 | } 171 | .tint(.accentColor) 172 | } 173 | ) 174 | } 175 | } 176 | return __macro_local_20__🤖🛠️_StorybookMagic_fMu_.bookBody.destination 177 | }() 178 | } 179 | """ 180 | } 181 | } 182 | 183 | func testUnlabeledTitleLabeledContent() { 184 | assertMacro { 185 | """ 186 | #Preview { 187 | #StorybookPreview( 188 | contents: { 189 | VStack { 190 | Text("test") 191 | } 192 | .tint(.accentColor) 193 | } 194 | ) 195 | } 196 | """ 197 | } expansion: { 198 | """ 199 | #Preview { 200 | { 201 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 202 | @MainActor 203 | static var bookBody: BookPage { 204 | .init( 205 | title: _typeName(UIView.self), 206 | destination: { 207 | VStack { 208 | Text("test") 209 | } 210 | .tint(.accentColor) 211 | } 212 | ) 213 | } 214 | } 215 | return __macro_local_20__🤖🛠️_StorybookMagic_fMu_.bookBody.destination 216 | }() 217 | } 218 | """ 219 | } 220 | 221 | assertMacro { 222 | """ 223 | #Preview { 224 | #StorybookPreview( 225 | title: "Path1.Path2.Title", 226 | contents: { 227 | VStack { 228 | Text("test") 229 | } 230 | .tint(.accentColor) 231 | } 232 | ) 233 | } 234 | """ 235 | } expansion: { 236 | """ 237 | #Preview { 238 | { 239 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 240 | @MainActor 241 | static var bookBody: BookPage { 242 | .init( 243 | title: "Path1.Path2.Title", 244 | destination: { 245 | VStack { 246 | Text("test") 247 | } 248 | .tint(.accentColor) 249 | } 250 | ) 251 | } 252 | } 253 | return __macro_local_20__🤖🛠️_StorybookMagic_fMu_.bookBody.destination 254 | }() 255 | } 256 | """ 257 | } 258 | } 259 | 260 | func testLabeledTitleLabeledContent() { 261 | assertMacro { 262 | """ 263 | enum Namespace1 { enum Namespace2 { enum Namespace3 { class TestableView: UIView {} } } } 264 | #Preview { 265 | #StorybookPreview( 266 | target: Namespace1.Namespace2.Namespace3.TestableView.self, 267 | contents: { 268 | VStack { 269 | Text("test") 270 | } 271 | .tint(.accentColor) 272 | } 273 | ) 274 | } 275 | """ 276 | } expansion: { 277 | """ 278 | enum Namespace1 { enum Namespace2 { enum Namespace3 { class TestableView: UIView {} } } } 279 | #Preview { 280 | { 281 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 282 | @MainActor 283 | static var bookBody: BookPage { 284 | .init( 285 | title: _typeName( 286 | target: Namespace1.Namespace2.Namespace3.TestableView.self,), 287 | destination: { 288 | VStack { 289 | Text("test") 290 | } 291 | .tint(.accentColor) 292 | } 293 | ) 294 | } 295 | } 296 | return __macro_local_20__🤖🛠️_StorybookMagic_fMu_.bookBody.destination 297 | }() 298 | } 299 | """ 300 | } 301 | 302 | assertMacro { 303 | """ 304 | #Preview { 305 | #StorybookPreview( 306 | title: "Path1.Path2.Title", 307 | contents: { 308 | VStack { 309 | Text("test") 310 | } 311 | .tint(.accentColor) 312 | } 313 | ) 314 | } 315 | """ 316 | } expansion: { 317 | """ 318 | #Preview { 319 | { 320 | enum __macro_local_20__🤖🛠️_StorybookMagic_fMu_: BookProvider { 321 | @MainActor 322 | static var bookBody: BookPage { 323 | .init( 324 | title: "Path1.Path2.Title", 325 | destination: { 326 | VStack { 327 | Text("test") 328 | } 329 | .tint(.accentColor) 330 | } 331 | ) 332 | } 333 | } 334 | return __macro_local_20__🤖🛠️_StorybookMagic_fMu_.bookBody.destination 335 | }() 336 | } 337 | """ 338 | } 339 | } 340 | } 341 | --------------------------------------------------------------------------------