├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Example
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Base.lproj
│ └── LaunchScreen.storyboard
├── ContentView.swift
├── Info.plist
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── SceneDelegate.swift
├── SwiftGUI.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── Example.xcscheme
│ │ └── SwiftGUI.xcscheme
└── URLImagePropertyPreview.swift
├── Package.resolved
├── Package.swift
├── README.md
├── Screenshot.png
└── Sources
└── SwiftGUI
├── Config.swift
├── Property.swift
├── SanitizedType.swift
├── SwiftUIExtensions.swift
├── TestObject.swift
└── Views
├── ArrayView.swift
├── DefaultPropertyPreviews.swift
├── EnumView.swift
├── ObjectView.swift
├── OptionalView.swift
├── PropertyPreview.swift
├── SwiftView.swift
└── UnknownView.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | **/xcuserdata
2 | **/.DS_Store
3 | .build
4 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Example
4 | //
5 | // Created by Yonas Kolb on 25/2/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | // Override point for customization after application launch.
16 | return true
17 | }
18 |
19 | // MARK: UISceneSession Lifecycle
20 |
21 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
22 | // Called when a new scene session is being created.
23 | // Use this method to select a configuration to create the new scene with.
24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
25 | }
26 |
27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
28 | // Called when the user discards a scene session.
29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
31 | }
32 |
33 |
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Example/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Example
4 | //
5 | // Created by Yonas Kolb on 25/2/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import SwiftGUI
11 |
12 | struct ContentView: View {
13 |
14 | let config: Config
15 | @State var object: TestObject? = TestObject()
16 | @State var openObject = false
17 |
18 | var objectDump: String {
19 | guard let object = object else { return ""}
20 | var string = ""
21 | dump(object, to: &string)
22 | return string
23 | }
24 |
25 | var body: some View {
26 | ScrollView {
27 | VStack(spacing: 20) {
28 | Button("Open Object") {
29 | self.openObject = true
30 | }
31 | Text(objectDump)
32 | .sheet(isPresented: $openObject) {
33 | NavigationView {
34 | SwiftView(value: self.$object, config: self.config)
35 | }
36 | }
37 | }.padding()
38 | }
39 | }
40 | }
41 |
42 | struct ContentView_Previews: PreviewProvider {
43 | static var previews: some View {
44 | ContentView(config: Config())
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Example/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 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 |
37 |
38 |
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UISupportedInterfaceOrientations~ipad
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationPortraitUpsideDown
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Example
4 | //
5 | // Created by Yonas Kolb on 25/2/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 | import SwiftGUI
12 |
13 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
14 |
15 | var window: UIWindow?
16 |
17 |
18 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
19 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
20 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
21 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
22 |
23 | // Create the SwiftUI view that provides the window contents.
24 | let config = Config()
25 | config.editing = true
26 | config.allowEditingToggle = true
27 | config.addPropertyPreview(URLImagePropertyPreview())
28 |
29 | let contentView = ContentView(config: config)
30 |
31 | // Use a UIHostingController as window root view controller.
32 | if let windowScene = scene as? UIWindowScene {
33 | let window = UIWindow(windowScene: windowScene)
34 | window.rootViewController = UIHostingController(rootView: contentView)
35 | self.window = window
36 | window.makeKeyAndVisible()
37 | }
38 | }
39 |
40 | func sceneDidDisconnect(_ scene: UIScene) {
41 | // Called as the scene is being released by the system.
42 | // This occurs shortly after the scene enters the background, or when its session is discarded.
43 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
44 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
45 | }
46 |
47 | func sceneDidBecomeActive(_ scene: UIScene) {
48 | // Called when the scene has moved from an inactive state to an active state.
49 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
50 | }
51 |
52 | func sceneWillResignActive(_ scene: UIScene) {
53 | // Called when the scene will move from an active state to an inactive state.
54 | // This may occur due to temporary interruptions (ex. an incoming phone call).
55 | }
56 |
57 | func sceneWillEnterForeground(_ scene: UIScene) {
58 | // Called as the scene transitions from the background to the foreground.
59 | // Use this method to undo the changes made on entering the background.
60 | }
61 |
62 | func sceneDidEnterBackground(_ scene: UIScene) {
63 | // Called as the scene transitions from the foreground to the background.
64 | // Use this method to save data, release shared resources, and store enough scene-specific state information
65 | // to restore the scene back to its current state.
66 | }
67 |
68 |
69 | }
70 |
71 |
--------------------------------------------------------------------------------
/Example/SwiftGUI.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 02699F8F25E4A06C0062884D /* SwiftGUI in Frameworks */ = {isa = PBXBuildFile; productRef = 02699F8E25E4A06C0062884D /* SwiftGUI */; };
11 | CC0D55A124112B650045A55D /* URLImagePropertyPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0D55A024112B650045A55D /* URLImagePropertyPreview.swift */; };
12 | CC0D55A3241130820045A55D /* URLImage in Frameworks */ = {isa = PBXBuildFile; productRef = CC0D55A2241130820045A55D /* URLImage */; };
13 | CC15795524049F7700185C2E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC15795424049F7700185C2E /* AppDelegate.swift */; };
14 | CC15795724049F7700185C2E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC15795624049F7700185C2E /* SceneDelegate.swift */; };
15 | CC15795924049F7700185C2E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC15795824049F7700185C2E /* ContentView.swift */; };
16 | CC15795B24049F7C00185C2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CC15795A24049F7C00185C2E /* Assets.xcassets */; };
17 | CC15795E24049F7C00185C2E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CC15795D24049F7C00185C2E /* Preview Assets.xcassets */; };
18 | CC15796124049F7C00185C2E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CC15795F24049F7C00185C2E /* LaunchScreen.storyboard */; };
19 | /* End PBXBuildFile section */
20 |
21 | /* Begin PBXCopyFilesBuildPhase section */
22 | CC15796B24049F9100185C2E /* Embed Frameworks */ = {
23 | isa = PBXCopyFilesBuildPhase;
24 | buildActionMask = 2147483647;
25 | dstPath = "";
26 | dstSubfolderSpec = 10;
27 | files = (
28 | );
29 | name = "Embed Frameworks";
30 | runOnlyForDeploymentPostprocessing = 0;
31 | };
32 | /* End PBXCopyFilesBuildPhase section */
33 |
34 | /* Begin PBXFileReference section */
35 | 02699F8D25E4A04A0062884D /* SwiftGUI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SwiftGUI; path = ..; sourceTree = ""; };
36 | CC0D55A024112B650045A55D /* URLImagePropertyPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLImagePropertyPreview.swift; sourceTree = ""; };
37 | CC15795224049F7700185C2E /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
38 | CC15795424049F7700185C2E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
39 | CC15795624049F7700185C2E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
40 | CC15795824049F7700185C2E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
41 | CC15795A24049F7C00185C2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
42 | CC15795D24049F7C00185C2E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
43 | CC15796024049F7C00185C2E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
44 | CC15796224049F7C00185C2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
45 | /* End PBXFileReference section */
46 |
47 | /* Begin PBXFrameworksBuildPhase section */
48 | CC15794F24049F7700185C2E /* Frameworks */ = {
49 | isa = PBXFrameworksBuildPhase;
50 | buildActionMask = 2147483647;
51 | files = (
52 | 02699F8F25E4A06C0062884D /* SwiftGUI in Frameworks */,
53 | CC0D55A3241130820045A55D /* URLImage in Frameworks */,
54 | );
55 | runOnlyForDeploymentPostprocessing = 0;
56 | };
57 | /* End PBXFrameworksBuildPhase section */
58 |
59 | /* Begin PBXGroup section */
60 | CC15792D2404994600185C2E = {
61 | isa = PBXGroup;
62 | children = (
63 | 02699F8D25E4A04A0062884D /* SwiftGUI */,
64 | CC15795324049F7700185C2E /* Example */,
65 | CC1579382404994600185C2E /* Products */,
66 | CC15796624049F9100185C2E /* Frameworks */,
67 | );
68 | sourceTree = "";
69 | };
70 | CC1579382404994600185C2E /* Products */ = {
71 | isa = PBXGroup;
72 | children = (
73 | CC15795224049F7700185C2E /* Example.app */,
74 | );
75 | name = Products;
76 | sourceTree = "";
77 | };
78 | CC15795324049F7700185C2E /* Example */ = {
79 | isa = PBXGroup;
80 | children = (
81 | CC15795424049F7700185C2E /* AppDelegate.swift */,
82 | CC15795624049F7700185C2E /* SceneDelegate.swift */,
83 | CC15795824049F7700185C2E /* ContentView.swift */,
84 | CC15795A24049F7C00185C2E /* Assets.xcassets */,
85 | CC15795F24049F7C00185C2E /* LaunchScreen.storyboard */,
86 | CC15796224049F7C00185C2E /* Info.plist */,
87 | CC15795C24049F7C00185C2E /* Preview Content */,
88 | CC0D55A024112B650045A55D /* URLImagePropertyPreview.swift */,
89 | );
90 | name = Example;
91 | sourceTree = "";
92 | };
93 | CC15795C24049F7C00185C2E /* Preview Content */ = {
94 | isa = PBXGroup;
95 | children = (
96 | CC15795D24049F7C00185C2E /* Preview Assets.xcassets */,
97 | );
98 | path = "Preview Content";
99 | sourceTree = "";
100 | };
101 | CC15796624049F9100185C2E /* Frameworks */ = {
102 | isa = PBXGroup;
103 | children = (
104 | );
105 | name = Frameworks;
106 | sourceTree = "";
107 | };
108 | /* End PBXGroup section */
109 |
110 | /* Begin PBXNativeTarget section */
111 | CC15795124049F7700185C2E /* Example */ = {
112 | isa = PBXNativeTarget;
113 | buildConfigurationList = CC15796324049F7C00185C2E /* Build configuration list for PBXNativeTarget "Example" */;
114 | buildPhases = (
115 | CC15794E24049F7700185C2E /* Sources */,
116 | CC15794F24049F7700185C2E /* Frameworks */,
117 | CC15795024049F7700185C2E /* Resources */,
118 | CC15796B24049F9100185C2E /* Embed Frameworks */,
119 | );
120 | buildRules = (
121 | );
122 | dependencies = (
123 | );
124 | name = Example;
125 | packageProductDependencies = (
126 | CC0D55A2241130820045A55D /* URLImage */,
127 | 02699F8E25E4A06C0062884D /* SwiftGUI */,
128 | );
129 | productName = Example;
130 | productReference = CC15795224049F7700185C2E /* Example.app */;
131 | productType = "com.apple.product-type.application";
132 | };
133 | /* End PBXNativeTarget section */
134 |
135 | /* Begin PBXProject section */
136 | CC15792E2404994600185C2E /* Project object */ = {
137 | isa = PBXProject;
138 | attributes = {
139 | LastSwiftUpdateCheck = 1140;
140 | LastUpgradeCheck = 1200;
141 | ORGANIZATIONNAME = Yonas;
142 | TargetAttributes = {
143 | CC15795124049F7700185C2E = {
144 | CreatedOnToolsVersion = 11.4;
145 | };
146 | };
147 | };
148 | buildConfigurationList = CC1579312404994600185C2E /* Build configuration list for PBXProject "SwiftGUI" */;
149 | compatibilityVersion = "Xcode 9.3";
150 | developmentRegion = en;
151 | hasScannedForEncodings = 0;
152 | knownRegions = (
153 | en,
154 | Base,
155 | );
156 | mainGroup = CC15792D2404994600185C2E;
157 | packageReferences = (
158 | CC15794524049E2900185C2E /* XCRemoteSwiftPackageReference "url-image" */,
159 | );
160 | productRefGroup = CC1579382404994600185C2E /* Products */;
161 | projectDirPath = "";
162 | projectRoot = "";
163 | targets = (
164 | CC15795124049F7700185C2E /* Example */,
165 | );
166 | };
167 | /* End PBXProject section */
168 |
169 | /* Begin PBXResourcesBuildPhase section */
170 | CC15795024049F7700185C2E /* Resources */ = {
171 | isa = PBXResourcesBuildPhase;
172 | buildActionMask = 2147483647;
173 | files = (
174 | CC15796124049F7C00185C2E /* LaunchScreen.storyboard in Resources */,
175 | CC15795E24049F7C00185C2E /* Preview Assets.xcassets in Resources */,
176 | CC15795B24049F7C00185C2E /* Assets.xcassets in Resources */,
177 | );
178 | runOnlyForDeploymentPostprocessing = 0;
179 | };
180 | /* End PBXResourcesBuildPhase section */
181 |
182 | /* Begin PBXSourcesBuildPhase section */
183 | CC15794E24049F7700185C2E /* Sources */ = {
184 | isa = PBXSourcesBuildPhase;
185 | buildActionMask = 2147483647;
186 | files = (
187 | CC15795524049F7700185C2E /* AppDelegate.swift in Sources */,
188 | CC0D55A124112B650045A55D /* URLImagePropertyPreview.swift in Sources */,
189 | CC15795724049F7700185C2E /* SceneDelegate.swift in Sources */,
190 | CC15795924049F7700185C2E /* ContentView.swift in Sources */,
191 | );
192 | runOnlyForDeploymentPostprocessing = 0;
193 | };
194 | /* End PBXSourcesBuildPhase section */
195 |
196 | /* Begin PBXVariantGroup section */
197 | CC15795F24049F7C00185C2E /* LaunchScreen.storyboard */ = {
198 | isa = PBXVariantGroup;
199 | children = (
200 | CC15796024049F7C00185C2E /* Base */,
201 | );
202 | name = LaunchScreen.storyboard;
203 | sourceTree = "";
204 | };
205 | /* End PBXVariantGroup section */
206 |
207 | /* Begin XCBuildConfiguration section */
208 | CC15793D2404994600185C2E /* Debug */ = {
209 | isa = XCBuildConfiguration;
210 | buildSettings = {
211 | ALWAYS_SEARCH_USER_PATHS = NO;
212 | CLANG_ANALYZER_NONNULL = YES;
213 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
215 | CLANG_CXX_LIBRARY = "libc++";
216 | CLANG_ENABLE_MODULES = YES;
217 | CLANG_ENABLE_OBJC_ARC = YES;
218 | CLANG_ENABLE_OBJC_WEAK = YES;
219 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
220 | CLANG_WARN_BOOL_CONVERSION = YES;
221 | CLANG_WARN_COMMA = YES;
222 | CLANG_WARN_CONSTANT_CONVERSION = YES;
223 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
224 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
225 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
226 | CLANG_WARN_EMPTY_BODY = YES;
227 | CLANG_WARN_ENUM_CONVERSION = YES;
228 | CLANG_WARN_INFINITE_RECURSION = YES;
229 | CLANG_WARN_INT_CONVERSION = YES;
230 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
231 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
232 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
233 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
234 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
235 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
236 | CLANG_WARN_STRICT_PROTOTYPES = YES;
237 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
238 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
239 | CLANG_WARN_UNREACHABLE_CODE = YES;
240 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
241 | COPY_PHASE_STRIP = NO;
242 | CURRENT_PROJECT_VERSION = 1;
243 | DEBUG_INFORMATION_FORMAT = dwarf;
244 | ENABLE_STRICT_OBJC_MSGSEND = YES;
245 | ENABLE_TESTABILITY = YES;
246 | GCC_C_LANGUAGE_STANDARD = gnu11;
247 | GCC_DYNAMIC_NO_PIC = NO;
248 | GCC_NO_COMMON_BLOCKS = YES;
249 | GCC_OPTIMIZATION_LEVEL = 0;
250 | GCC_PREPROCESSOR_DEFINITIONS = (
251 | "DEBUG=1",
252 | "$(inherited)",
253 | );
254 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
255 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
256 | GCC_WARN_UNDECLARED_SELECTOR = YES;
257 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
258 | GCC_WARN_UNUSED_FUNCTION = YES;
259 | GCC_WARN_UNUSED_VARIABLE = YES;
260 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
261 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
262 | MTL_FAST_MATH = YES;
263 | ONLY_ACTIVE_ARCH = YES;
264 | SDKROOT = iphoneos;
265 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
266 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
267 | VERSIONING_SYSTEM = "apple-generic";
268 | VERSION_INFO_PREFIX = "";
269 | };
270 | name = Debug;
271 | };
272 | CC15793E2404994600185C2E /* Release */ = {
273 | isa = XCBuildConfiguration;
274 | buildSettings = {
275 | ALWAYS_SEARCH_USER_PATHS = NO;
276 | CLANG_ANALYZER_NONNULL = YES;
277 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
278 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
279 | CLANG_CXX_LIBRARY = "libc++";
280 | CLANG_ENABLE_MODULES = YES;
281 | CLANG_ENABLE_OBJC_ARC = YES;
282 | CLANG_ENABLE_OBJC_WEAK = YES;
283 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
284 | CLANG_WARN_BOOL_CONVERSION = YES;
285 | CLANG_WARN_COMMA = YES;
286 | CLANG_WARN_CONSTANT_CONVERSION = YES;
287 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
288 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
289 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
290 | CLANG_WARN_EMPTY_BODY = YES;
291 | CLANG_WARN_ENUM_CONVERSION = YES;
292 | CLANG_WARN_INFINITE_RECURSION = YES;
293 | CLANG_WARN_INT_CONVERSION = YES;
294 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
295 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
296 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
297 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
298 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
299 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
300 | CLANG_WARN_STRICT_PROTOTYPES = YES;
301 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
302 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
303 | CLANG_WARN_UNREACHABLE_CODE = YES;
304 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
305 | COPY_PHASE_STRIP = NO;
306 | CURRENT_PROJECT_VERSION = 1;
307 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
308 | ENABLE_NS_ASSERTIONS = NO;
309 | ENABLE_STRICT_OBJC_MSGSEND = YES;
310 | GCC_C_LANGUAGE_STANDARD = gnu11;
311 | GCC_NO_COMMON_BLOCKS = YES;
312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
314 | GCC_WARN_UNDECLARED_SELECTOR = YES;
315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
316 | GCC_WARN_UNUSED_FUNCTION = YES;
317 | GCC_WARN_UNUSED_VARIABLE = YES;
318 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
319 | MTL_ENABLE_DEBUG_INFO = NO;
320 | MTL_FAST_MATH = YES;
321 | SDKROOT = iphoneos;
322 | SWIFT_COMPILATION_MODE = wholemodule;
323 | SWIFT_OPTIMIZATION_LEVEL = "-O";
324 | VALIDATE_PRODUCT = YES;
325 | VERSIONING_SYSTEM = "apple-generic";
326 | VERSION_INFO_PREFIX = "";
327 | };
328 | name = Release;
329 | };
330 | CC15796424049F7C00185C2E /* Debug */ = {
331 | isa = XCBuildConfiguration;
332 | buildSettings = {
333 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
334 | CODE_SIGN_STYLE = Automatic;
335 | DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
336 | ENABLE_PREVIEWS = YES;
337 | INFOPLIST_FILE = Info.plist;
338 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
339 | LD_RUNPATH_SEARCH_PATHS = (
340 | "$(inherited)",
341 | "@executable_path/Frameworks",
342 | );
343 | PRODUCT_BUNDLE_IDENTIFIER = com.yonaskolb.Example;
344 | PRODUCT_NAME = "$(TARGET_NAME)";
345 | SWIFT_VERSION = 5.0;
346 | TARGETED_DEVICE_FAMILY = "1,2";
347 | };
348 | name = Debug;
349 | };
350 | CC15796524049F7C00185C2E /* Release */ = {
351 | isa = XCBuildConfiguration;
352 | buildSettings = {
353 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
354 | CODE_SIGN_STYLE = Automatic;
355 | DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
356 | ENABLE_PREVIEWS = YES;
357 | INFOPLIST_FILE = Info.plist;
358 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
359 | LD_RUNPATH_SEARCH_PATHS = (
360 | "$(inherited)",
361 | "@executable_path/Frameworks",
362 | );
363 | PRODUCT_BUNDLE_IDENTIFIER = com.yonaskolb.Example;
364 | PRODUCT_NAME = "$(TARGET_NAME)";
365 | SWIFT_VERSION = 5.0;
366 | TARGETED_DEVICE_FAMILY = "1,2";
367 | };
368 | name = Release;
369 | };
370 | /* End XCBuildConfiguration section */
371 |
372 | /* Begin XCConfigurationList section */
373 | CC1579312404994600185C2E /* Build configuration list for PBXProject "SwiftGUI" */ = {
374 | isa = XCConfigurationList;
375 | buildConfigurations = (
376 | CC15793D2404994600185C2E /* Debug */,
377 | CC15793E2404994600185C2E /* Release */,
378 | );
379 | defaultConfigurationIsVisible = 0;
380 | defaultConfigurationName = Release;
381 | };
382 | CC15796324049F7C00185C2E /* Build configuration list for PBXNativeTarget "Example" */ = {
383 | isa = XCConfigurationList;
384 | buildConfigurations = (
385 | CC15796424049F7C00185C2E /* Debug */,
386 | CC15796524049F7C00185C2E /* Release */,
387 | );
388 | defaultConfigurationIsVisible = 0;
389 | defaultConfigurationName = Release;
390 | };
391 | /* End XCConfigurationList section */
392 |
393 | /* Begin XCRemoteSwiftPackageReference section */
394 | CC15794524049E2900185C2E /* XCRemoteSwiftPackageReference "url-image" */ = {
395 | isa = XCRemoteSwiftPackageReference;
396 | repositoryURL = "https://github.com/dmytro-anokhin/url-image";
397 | requirement = {
398 | kind = upToNextMajorVersion;
399 | minimumVersion = 0.9.10;
400 | };
401 | };
402 | /* End XCRemoteSwiftPackageReference section */
403 |
404 | /* Begin XCSwiftPackageProductDependency section */
405 | 02699F8E25E4A06C0062884D /* SwiftGUI */ = {
406 | isa = XCSwiftPackageProductDependency;
407 | productName = SwiftGUI;
408 | };
409 | CC0D55A2241130820045A55D /* URLImage */ = {
410 | isa = XCSwiftPackageProductDependency;
411 | package = CC15794524049E2900185C2E /* XCRemoteSwiftPackageReference "url-image" */;
412 | productName = URLImage;
413 | };
414 | /* End XCSwiftPackageProductDependency section */
415 | };
416 | rootObject = CC15792E2404994600185C2E /* Project object */;
417 | }
418 |
--------------------------------------------------------------------------------
/Example/SwiftGUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/SwiftGUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/SwiftGUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Runtime",
6 | "repositoryURL": "https://github.com/wickwirew/Runtime",
7 | "state": {
8 | "branch": null,
9 | "revision": "dad03135d7701a4e7b3a4051e75d6b37bd8e178e",
10 | "version": "2.2.4"
11 | }
12 | },
13 | {
14 | "package": "URLImage",
15 | "repositoryURL": "https://github.com/dmytro-anokhin/url-image",
16 | "state": {
17 | "branch": null,
18 | "revision": "dc913391b6fc0c9de283ef111eb90d0e9abf4c36",
19 | "version": "0.9.10"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/Example/SwiftGUI.xcodeproj/xcshareddata/xcschemes/Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Example/SwiftGUI.xcodeproj/xcshareddata/xcschemes/SwiftGUI.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
53 |
54 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/Example/URLImagePropertyPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLImagePropertyPreview.swift
3 | // Example
4 | //
5 | // Created by Yonas Kolb on 5/3/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import SwiftGUI
11 | import URLImage
12 |
13 | struct URLImagePropertyPreview: PropertyPreview {
14 |
15 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig {
16 | let imageNames: [String] = [
17 | "picture",
18 | "profilepic",
19 | "avatar",
20 | "profileimage",
21 | ]
22 | let isImageURL = imageNames.contains { property.name.lowercased().contains($0.lowercased()) }
23 | return PropertyPreviewConfig(showPropertyPreview: isImageURL, customView: true)
24 | }
25 |
26 | func body(context: PropertyPreviewContext) -> some View {
27 | let url = context.value
28 |
29 | let imageView = URLImage(url) { proxy in
30 | proxy.image
31 | .resizable()
32 | .frame(width: 30, height: 30)
33 | .aspectRatio(contentMode: .fill)
34 | .border(Color.gray, width: 1)
35 | }
36 |
37 | let stringBinding = context.binding.map(
38 | get: { $0.absoluteString },
39 | set: { URL(string: $0)! }
40 | )
41 |
42 | return Group {
43 |
44 | if context.config.editing {
45 | HStack {
46 | TextField("", text: stringBinding)
47 | .textFieldStyle(RoundedBorderTextFieldStyle())
48 | .keyboardType(.URL)
49 | imageView
50 | }
51 | } else {
52 | imageView
53 | }
54 | }
55 | }
56 |
57 | func child(context: PropertyPreviewContext) -> some View {
58 | return URLImage(context.value) { proxy in
59 | proxy.image
60 | .resizable()
61 | .aspectRatio(contentMode: .fit)
62 | }
63 | .navigationBarTitle(context.property.name)
64 | }
65 | }
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Runtime",
6 | "repositoryURL": "https://github.com/wickwirew/Runtime",
7 | "state": {
8 | "branch": null,
9 | "revision": "dad03135d7701a4e7b3a4051e75d6b37bd8e178e",
10 | "version": "2.2.4"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SwiftGUI",
8 | platforms: [
9 | .iOS(.v14), .macOS(.v11),
10 | ],
11 | products: [
12 | .library(
13 | name: "SwiftGUI",
14 | targets: ["SwiftGUI"]),
15 | ],
16 | dependencies: [
17 | .package(name: "Runtime", url: "https://github.com/wickwirew/Runtime", from: "2.2.4"),
18 | ],
19 | targets: [
20 | .target(
21 | name: "SwiftGUI",
22 | dependencies: ["Runtime"]),
23 | ]
24 | )
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftGUI
2 |
3 | A SwiftUI GUI for browsing and editing pure Swift objects. It's useful as a debug tool, especially in single state apps as you can browse and edit your whole app state.
4 |
5 | It supports Swift structs, classes, and enums with associated types.
6 |
7 |
8 |
9 | ## Development
10 | Note that this project is still in development. It is missing features, and could be unstable.
11 |
--------------------------------------------------------------------------------
/Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yonaskolb/SwiftGUI/e0d07fc374fd1d999c58675cb8991803f3c615c0/Screenshot.png
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Config.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Config.swift
3 | // SwiftGUI
4 | //
5 | // Created by Yonas Kolb on 26/2/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | public class Config: ObservableObject {
13 |
14 | @Published public var editing: Bool
15 | @Published public var allowEditingToggle: Bool
16 | /// filters a property name. Return nil to filter out property entirely
17 | @Published public var propertyFilter: (String) -> String?
18 |
19 | var propertyPreviews: [PropertyPreviewRenderer] = []
20 |
21 | // replaces underscore prefixes and removes "$" properties (like "_$observationRegistrar")
22 | public static func prettyPropertyFilter(property: String) -> String? {
23 | var name = property
24 | if name.hasPrefix("_") {
25 | name = String(name.dropFirst())
26 | }
27 | if name.hasPrefix("$") {
28 | return nil
29 | }
30 | return name
31 | }
32 |
33 | public init(
34 | editing: Bool = true,
35 | allowEditingToggle: Bool = false,
36 | addDefaultProperties: Bool = true,
37 | propertyFilter: @escaping (String) -> String? = { $0 }
38 | ) {
39 | self.editing = editing
40 | self.allowEditingToggle = allowEditingToggle
41 | self.propertyFilter = propertyFilter
42 | if addDefaultProperties {
43 | addDefaultPropertyPreviews()
44 | }
45 | }
46 |
47 | public func addDefaultPropertyPreviews() {
48 | addPropertyPreview(BoolPropertyPreview())
49 | addPropertyPreview(DatePropertyPreview())
50 | addPropertyPreview(ArrayPropertyPreview())
51 | addPropertyPreview(StringPropertyPreview())
52 | addPropertyPreview(IntPropertyPreview())
53 | addPropertyPreview(DoublePropertyPreview())
54 | addPropertyPreview(DictionaryPropertyPreview())
55 | addPropertyPreview(URLPropertyPreview())
56 | }
57 |
58 | public func addPropertyPreview(_ preview: P) {
59 | let render: (PropertyPreviewContext) -> AnyView = { context in
60 | let binding: Binding = context.binding.map(
61 | get: { $0 as! P.Value },
62 | set: { $0 }
63 | )
64 | let newContext = PropertyPreviewContext(property: context.property, config: context.config, propertyConfig: context.propertyConfig, binding: binding)
65 | let view = preview.body(context: newContext)
66 | return view.anyView
67 | }
68 | let childRender: (PropertyPreviewContext) -> AnyView = { context in
69 | let binding: Binding = context.binding.map(
70 | get: { $0 as! P.Value },
71 | set: { $0 }
72 | )
73 | let newContext = PropertyPreviewContext(property: context.property, config: context.config, propertyConfig: context.propertyConfig, binding: binding)
74 | return preview.child(context: newContext).anyView
75 | }
76 | let typeName = String(describing: P.Value.self)
77 | let renderer = PropertyPreviewRenderer(getConfig: preview.getConfig, render: render, childRender: childRender, typeName: typeName)
78 | propertyPreviews.insert(renderer, at: 0)
79 | }
80 |
81 | public func getPreview(for property: Property, with binding: Binding) -> PropertyPreviewResult? {
82 | guard let value = property.value else { return nil }
83 | let typeName = String(describing: type(of: value))
84 | for propertyPreview in propertyPreviews {
85 | let config = propertyPreview.getConfig(property, self)
86 | let context = PropertyPreviewContext(property: property, config: self, propertyConfig: config, binding: binding)
87 | let typeMatch = config.matchType ? (typeName == propertyPreview.typeName) : true
88 | if typeMatch && config.showPropertyPreview {
89 | let view = propertyPreview.render(context).anyView
90 | let childView = config.customView ? propertyPreview.childRender(context) : nil
91 | return PropertyPreviewResult(view: view, config: config, childView: childView)
92 | }
93 | }
94 | return nil
95 | }
96 | }
97 |
98 | public struct PropertyPreviewResult {
99 | public let view: AnyView
100 | public let config: PropertyPreviewConfig
101 | public let childView: AnyView?
102 | }
103 |
104 | struct PropertyPreviewRenderer {
105 |
106 | let getConfig: (Property, Config) -> PropertyPreviewConfig
107 | let render: (PropertyPreviewContext) -> AnyView
108 | let childRender: (PropertyPreviewContext) -> AnyView
109 | let typeName: String?
110 | }
111 |
112 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Property.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Runtime
3 |
4 | public struct Property: Identifiable, CustomStringConvertible {
5 |
6 | public let typeName: String
7 | public let propertyInfo: PropertyInfo
8 | public var typeInfo: TypeInfo
9 | public let value: Any?
10 |
11 | public var isOptional: Bool { typeInfo.kind == .optional }
12 | public var name: String { propertyInfo.name }
13 | public var description: String { id }
14 | public var id: String { "\(name): \(typeName)" }
15 |
16 | public init(type: Any, propertyInfo: PropertyInfo) throws {
17 | self.propertyInfo = propertyInfo
18 | self.typeName = santizedType(of: propertyInfo.type)
19 | self.value = try propertyInfo.get(from: type)
20 | self.typeInfo = try Runtime.typeInfo(of: propertyInfo.type)
21 | if let value = value {
22 | self.typeInfo = try Runtime.typeInfo(of: Swift.type(of: value))
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/SanitizedType.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public func santizedType(from string: String) -> String {
4 | return string
5 | .replacingOccurrences(of: "Array<(.+)>", with: "[$1]", options: [.regularExpression])
6 | .replacingOccurrences(of: "Array<(.+)>", with: "[$1]", options: [.regularExpression])
7 | .replacingOccurrences(of: "Array<(.+)>", with: "[$1]", options: [.regularExpression])
8 | .replacingOccurrences(of: "Dictionary<(.+), (.+)>", with: "[$1: $2]", options: [.regularExpression])
9 | .replacingOccurrences(of: "Dictionary<(.+), (.+)>", with: "[$1: $2]", options: [.regularExpression])
10 | .replacingOccurrences(of: "Dictionary<(.+), (.+)>", with: "[$1: $2]", options: [.regularExpression])
11 | .replacingOccurrences(of: "Optional<(.+)>", with: "$1?", options: [.regularExpression])
12 | .replacingOccurrences(of: "Optional<(.+)>", with: "$1?", options: [.regularExpression])
13 | .replacingOccurrences(of: "Optional<(.+)>", with: "$1?", options: [.regularExpression])
14 | }
15 |
16 | public func santizedType(of type: Any.Type) -> String {
17 | return santizedType(from: String(describing: type))
18 | }
19 |
20 | public func santizedType(of object: Any) -> String {
21 | return santizedType(of: type(of: object))
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/SwiftUIExtensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | extension View {
5 |
6 | var anyView: AnyView {
7 | AnyView(self)
8 | }
9 |
10 | }
11 |
12 | extension Binding {
13 |
14 | public func map(get: @escaping (Value) -> T, set: @escaping (T) -> Value) -> Binding {
15 | Binding(
16 | get: { get(self.wrappedValue) },
17 | set: { self.wrappedValue = set($0) }
18 | )
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/TestObject.swift:
--------------------------------------------------------------------------------
1 |
2 | import Foundation
3 |
4 | public struct TestObject {
5 |
6 | public var myBool: Bool = true
7 | public var myString: String = "hello"
8 | public var myInt: Int = 5
9 | public var myDate: Date = Date()
10 | public var myArray: [String] = ["one", "two"]
11 | public var myObjectArray: [TestStruct] = [TestStruct(name: "Sarah"), TestStruct(name: "Jade")]
12 | public var myDictionary: [String: Int] = ["one": 1, "two": 2]
13 | public var myURL: URL = URL(string: "http://google.com")!
14 | public var myAvatarURL: URL = URL(string: "https://randomuser.me/api/portraits/women/10.jpg")!
15 | public var myStruct: TestStruct = TestStruct()
16 | public var myClass: TestClass = TestClass()
17 | public var myRawEnum: RawEnum = .one
18 | public var myIntEnum: AssociatedEnum = .int(1)
19 | public var myObjectEnum: AssociatedEnum = .object(TestStruct())
20 | public var myMultiEnum: AssociatedEnum = .multi(true, object: TestStruct())
21 |
22 | public var myOptionalBool: Bool? = true
23 | public var myOptionalString: String? = "hello"
24 | public var myOptionalInt: Int? = 5
25 | public var myOptionalDate: Date? = Date()
26 | public var myOptionalURL: URL? = URL(string: "http://google.com")
27 | public var myOptionalAvatarURL: URL? = URL(string: "https://randomuser.me/api/portraits/women/10.jpg")
28 | public var myOptionalStruct: TestStruct? = TestStruct()
29 | public var myOptionalClass: TestClass? = TestClass()
30 | public var myOptionalRawEnum: RawEnum? = .one
31 |
32 | public var myNilBool: Bool?
33 | public var myNilString: String?
34 | public var myNilInt: Int?
35 | public var myNilDate: Date?
36 | public var myNilURL: URL?
37 | public var myNilAvatarURL: URL?
38 | public var myNilStruct: TestStruct?
39 | public var myNilClass: TestClass?
40 |
41 | public init() {
42 |
43 | }
44 | }
45 |
46 | public struct TestStruct: CustomStringConvertible {
47 | var name: String = "John"
48 | public var description: String { name }
49 | }
50 |
51 | public class TestClass {
52 | var name: String = "John"
53 | }
54 |
55 | public enum RawEnum: Int {
56 | case one
57 | case two
58 | }
59 |
60 | public enum AssociatedEnum {
61 | case int(Int)
62 | case object(TestStruct)
63 | case multi(Bool, object: TestStruct)
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Views/ArrayView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArrayView.swift
3 | // SwiftGUI
4 | //
5 | // Created by Yonas Kolb on 26/2/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Runtime
11 |
12 | struct ArrayView: View {
13 |
14 | @Binding var array: [Any]
15 | @EnvironmentObject var config: Config
16 |
17 | init(array: Binding<[Any]>) {
18 | self._array = array
19 | }
20 |
21 | var body: some View {
22 | List(Array(0 ..< array.count), id: \.self) { index in
23 | Text("\(index): " + String(describing: self.array[index]))
24 | .lineLimit(1)
25 | .swiftLink(self.$array[index], config: config)
26 | }
27 | }
28 | }
29 |
30 | struct ArrayView_Previews: PreviewProvider {
31 | static var previews: some View {
32 | Group {
33 | ArrayView(array: .constant([TestStruct(), TestStruct()]))
34 | ArrayView(array: .constant([1, 2]))
35 | }
36 | .environmentObject(Config())
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Views/DefaultPropertyPreviews.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultPropertyPreviews.swift
3 | // SwiftGUI
4 | //
5 | // Created by Yonas Kolb on 6/3/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct BoolPropertyPreview: PropertyPreview {
13 |
14 | func body(context: PropertyPreviewContext) -> some View {
15 | Group {
16 | if context.config.editing {
17 | Toggle("", isOn: context.binding)
18 | } else {
19 | Text(context.value.description)
20 | }
21 | }
22 | }
23 | }
24 |
25 | struct StringPropertyPreview: PropertyPreview {
26 |
27 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig {
28 | PropertyPreviewConfig(canNavigate: true, customView: true)
29 | }
30 |
31 | func child(context: PropertyPreviewContext) -> some View {
32 | TextField(context.property.name, text: context.binding)
33 | .textFieldStyle(RoundedBorderTextFieldStyle())
34 | .padding()
35 | .frame(maxHeight: .infinity, alignment: .topLeading)
36 | }
37 | }
38 |
39 | struct URLPropertyPreview: PropertyPreview {
40 |
41 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig {
42 | PropertyPreviewConfig(canNavigate: true, customView: true)
43 | }
44 |
45 | func child(context: PropertyPreviewContext) -> some View {
46 | let stringBinding = context.binding.map(
47 | get: { $0.absoluteString },
48 | set: { URL(string: $0)! }
49 | )
50 | return TextField(context.property.name, text: stringBinding)
51 | .textFieldStyle(RoundedBorderTextFieldStyle())
52 | .padding()
53 | .frame(maxHeight: .infinity, alignment: .topLeading)
54 | }
55 | }
56 |
57 | struct IntPropertyPreview: PropertyPreview {
58 |
59 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig {
60 | PropertyPreviewConfig(canNavigate: config.editing, customView: true)
61 | }
62 |
63 | func child(context: PropertyPreviewContext) -> some View {
64 | let stringBinding = context.binding.map(
65 | get: { $0.description },
66 | set: { Int($0) ?? 0 }
67 | )
68 | return Group {
69 | if context.config.editing {
70 | TextField("", text: stringBinding)
71 | .textFieldStyle(RoundedBorderTextFieldStyle())
72 | #if os(iOS)
73 | .keyboardType(.numberPad)
74 | #endif
75 | .multilineTextAlignment(.trailing)
76 | } else {
77 | Text(context.value.description).font(.largeTitle)
78 | }
79 | }
80 | .padding()
81 | .frame(maxHeight: .infinity, alignment: .topLeading)
82 | }
83 | }
84 |
85 | struct DoublePropertyPreview: PropertyPreview {
86 |
87 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig {
88 | PropertyPreviewConfig(canNavigate: config.editing, customView: true)
89 | }
90 |
91 | func child(context: PropertyPreviewContext) -> some View {
92 | let stringBinding = context.binding.map(
93 | get: { $0.description },
94 | set: { Double($0) ?? 0 }
95 | )
96 | return Group {
97 | if context.config.editing {
98 | TextField("", text: stringBinding)
99 | .textFieldStyle(RoundedBorderTextFieldStyle())
100 | #if os(iOS)
101 | .keyboardType(.numberPad)
102 | #endif
103 | .multilineTextAlignment(.trailing)
104 | } else {
105 | Text(context.value.description).font(.largeTitle)
106 | }
107 | }
108 | .padding()
109 | .frame(maxHeight: .infinity, alignment: .topLeading)
110 | }
111 | }
112 |
113 | struct DatePropertyPreview: PropertyPreview {
114 |
115 | typealias Child = EmptyView
116 | static let dateFormatter: DateFormatter = {
117 | let dateFormatter = DateFormatter()
118 | dateFormatter.dateStyle = .short
119 | dateFormatter.timeStyle = .short
120 | return dateFormatter
121 | }()
122 |
123 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig {
124 | let axis: Axis = config.editing ? .vertical : .horizontal
125 | return PropertyPreviewConfig(axis: axis)
126 | }
127 |
128 | func body(context: PropertyPreviewContext) -> some View {
129 | Group {
130 | if context.config.editing {
131 | DatePicker("", selection: context.binding)
132 | .datePickerStyle(DefaultDatePickerStyle())
133 | } else {
134 | Text(Self.dateFormatter.string(from: context.value))
135 | }
136 | }
137 | }
138 | }
139 |
140 | struct ArrayPropertyPreview: PropertyPreview {
141 |
142 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig {
143 | PropertyPreviewConfig(matchType: false, showPropertyPreview: property.value is [Any], customView: true)
144 | }
145 |
146 | func body(context: PropertyPreviewContext<[Any]>) -> some View {
147 | Text(context.value.count.description)
148 | }
149 |
150 | func child(context: PropertyPreviewContext<[Any]>) -> some View {
151 | ArrayView(array: context.binding)
152 | }
153 | }
154 |
155 | struct DictionaryPropertyPreview: PropertyPreview {
156 |
157 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig {
158 | PropertyPreviewConfig(matchType: false, showPropertyPreview: property.value is [AnyHashable: Any])
159 | }
160 |
161 | func body(context: PropertyPreviewContext<[AnyHashable: Any]>) -> some View {
162 | Text(context.value.count.description)
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Views/EnumView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnumView.swift
3 | // SwiftGUI
4 | //
5 | // Created by Yonas Kolb on 3/3/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Runtime
11 |
12 | struct EnumView: View {
13 |
14 | @Binding var enumValue: Any
15 | let isRaw: Bool
16 |
17 | init(_ enumValueBinding: Binding) {
18 | self._enumValue = enumValueBinding
19 | let mirror = Mirror(reflecting: enumValueBinding.wrappedValue)
20 | isRaw = mirror.children.isEmpty
21 | }
22 |
23 | var body: some View {
24 | Group {
25 | if isRaw {
26 | EnumRawView($enumValue)
27 | } else {
28 | EnumAssociatedView($enumValue)
29 | }
30 | }
31 | }
32 | }
33 |
34 | private struct EnumRawView: View {
35 |
36 | @Binding var enumValue: Any
37 | let cases: [String]
38 | let selectedCase: String
39 |
40 | init(_ enumValueBinding: Binding) {
41 | let enumValue = enumValueBinding.wrappedValue
42 | self._enumValue = enumValueBinding
43 | let typeInfo = try! Runtime.typeInfo(of: type(of: enumValue))
44 | precondition(typeInfo.kind == .enum)
45 | cases = typeInfo.cases.map { $0.name }
46 | let mirror = Mirror(reflecting: enumValue)
47 | let children = mirror.children.map { $0 }
48 | selectedCase = children.first?.label ?? String(describing: enumValue)
49 | }
50 |
51 | var body: some View {
52 | List(cases, id: \.self) { enumCase in
53 | HStack {
54 | Text(enumCase)
55 | Spacer()
56 | if enumCase == self.selectedCase {
57 | Image(systemName: "checkmark.circle.fill").foregroundColor(.accentColor)
58 | } else {
59 | Image(systemName: "circle").foregroundColor(.gray)
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
66 | private struct EnumAssociatedView: View {
67 |
68 | @Binding var enumValue: Any
69 | @EnvironmentObject var config: Config
70 | let types: [AssociatedType]
71 | let singleValue: Any?
72 |
73 | struct AssociatedType {
74 | let label: String
75 | let type: Any.Type
76 | let typeName: String
77 | let value: Any
78 | let id: Int
79 | }
80 |
81 | init(_ enumValueBinding: Binding) {
82 | let enumValue = enumValueBinding.wrappedValue
83 | self._enumValue = enumValueBinding
84 | let mirror = Mirror(reflecting: enumValue)
85 | let children = mirror.children.map { $0 }
86 | precondition(children.count == 1)
87 | let associatedMirror = Mirror(reflecting: children[0].value)
88 | if associatedMirror.children.count == 0 {
89 | singleValue = children[0].value
90 | } else {
91 | singleValue = nil
92 | }
93 | types = associatedMirror.children.enumerated().map { index, value in
94 | let label = (value.label ?? "").replacingOccurrences(of: #"\.\d"#, with: "", options: [.regularExpression], range: nil)
95 | return AssociatedType(
96 | label: label,
97 | type: type(of: value.value),
98 | typeName: santizedType(of: value.value),
99 | value: value.value,
100 | id: index)
101 | }
102 | }
103 |
104 | var body: some View {
105 | Group {
106 | if singleValue != nil {
107 | UnknownView(value: .constant(singleValue!))
108 | } else {
109 | List(types, id: \.id) { type in
110 | HStack {
111 | (Text(type.label.isEmpty ? "" : "\(type.label): ") +
112 | Text(type.typeName).bold())
113 | Spacer()
114 | Text(String(describing: type.value))
115 | }
116 | .lineLimit(1)
117 | .swiftLink(.constant(type.value), config: config) // TODO: editable
118 | }
119 | }
120 | }
121 | }
122 | }
123 |
124 | struct EnumView_Previews: PreviewProvider {
125 | static var previews: some View {
126 | Group {
127 | EnumView(.constant(RawEnum.one))
128 | EnumView(.constant(AssociatedEnum.int(1)))
129 | EnumView(.constant(AssociatedEnum.object(TestStruct())))
130 | EnumView(.constant(AssociatedEnum.multi(true, object: TestStruct())))
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Views/ObjectView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Runtime
3 |
4 | struct ObjectView: View {
5 |
6 | private var name: String
7 | @Binding private var object: Any
8 | private var properties: [Property]
9 | private let typeInfo: TypeInfo
10 | private let isRoot: Bool
11 |
12 | @EnvironmentObject var config: Config
13 | @Environment(\.showRootNavTitle) var showRootNavTitle
14 |
15 | var filteredProperties: [Property] {
16 | properties.filter {
17 | config.propertyFilter($0.name) != nil
18 | }
19 | }
20 |
21 | init(_ binding: Binding, isRoot: Bool = false) {
22 | self.isRoot = isRoot
23 | let object = binding.wrappedValue
24 | self._object = binding
25 | self.typeInfo = try! Runtime.typeInfo(of: type(of: object))
26 | name = santizedType(of: object)
27 |
28 | properties = typeInfo.properties.compactMap {
29 | return try? Property(type: object, propertyInfo: $0)
30 | }
31 | }
32 |
33 | func getPropertyBinding(_ property: Property) -> Binding {
34 | Binding(
35 | get: { try! property.propertyInfo.get(from: self.object) },
36 | set: { try! property.propertyInfo.set(value: $0, on: &self.object) }
37 | )
38 | }
39 |
40 | func propertyRow(_ property: Property, axis: Axis = .horizontal, @ViewBuilder content: () -> V) -> some View {
41 |
42 | func info() -> some View {
43 | (Text("\(config.propertyFilter(property.name) ?? ""): ") + Text(property.typeName).bold())
44 | .lineLimit(1)
45 | }
46 |
47 | let contentView = content()
48 | .disabled(!config.editing)
49 |
50 | return Group {
51 | if axis == .horizontal {
52 | HStack {
53 | info()
54 | .layoutPriority(1)
55 | Spacer()
56 | contentView
57 | .lineLimit(1)
58 | }
59 | } else {
60 | VStack(alignment: .leading) {
61 | info()
62 | contentView
63 | }
64 | }
65 | }
66 | }
67 |
68 | func link(_ property: Property, @ViewBuilder destination: () -> D, @ViewBuilder content: () -> C) -> some View {
69 | NavigationLink {
70 | destination()
71 | .environmentObject(config)
72 | #if os(iOS)
73 | .navigationBarTitle(property.name)
74 | #endif
75 | } label: {
76 | content()
77 | }
78 | }
79 |
80 | @ViewBuilder
81 | func propertyEditor(_ property: Property) -> some View {
82 | if let preview = config.getPreview(for: property, with: getPropertyBinding(property)) {
83 | if preview.config.canNavigate || preview.config.customView {
84 | if let childView = preview.childView {
85 | link(property) {
86 | childView
87 | } content: {
88 | propertyRow(property, axis: preview.config.axis) {
89 | preview.view
90 | }
91 | }
92 | } else {
93 | link(property) {
94 | UnknownView(value: getPropertyBinding(property))
95 | } content: {
96 | propertyRow(property, axis: preview.config.axis) {
97 | preview.view
98 | }
99 | }
100 | }
101 | } else {
102 | propertyRow(property, axis: preview.config.axis) {
103 | preview.view
104 | }
105 | }
106 | } else if let value = property.value {
107 | link(property) {
108 | UnknownView(value: getPropertyBinding(property))
109 | } content: {
110 | propertyRow(property) {
111 | Text(String(describing: value)).lineLimit(1)
112 | }
113 | }
114 | } else {
115 | propertyRow(property) {
116 | Text("nil").foregroundColor(.gray)
117 | }
118 | }
119 | }
120 |
121 | var editButton: some View {
122 | Group {
123 | if config.allowEditingToggle {
124 | Button(config.editing ? "View" : "Edit") {
125 | withAnimation { self.config.editing.toggle() }
126 | }
127 | } else {
128 | EmptyView()
129 | }
130 | }
131 | }
132 |
133 | var body: some View {
134 | if !isRoot || showRootNavTitle {
135 | list
136 | #if os(iOS)
137 | .navigationBarTitle(Text(name), displayMode: .inline)
138 | #endif
139 | } else {
140 | list
141 | }
142 | }
143 |
144 | var list: some View {
145 | List(filteredProperties) { property in
146 | self.propertyEditor(property)
147 | }
148 | }
149 | }
150 |
151 | struct EditView_Previews: PreviewProvider {
152 |
153 | static var previews: some View {
154 | Group {
155 | NavigationView {
156 | ObjectView(.constant(TestObject()))
157 | .environmentObject(Config(editing: true))
158 | }
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Views/OptionalView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yonas Kolb on 24/10/2022.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | protocol OptionalProtocol {
12 | func isSome() -> Bool
13 | func unwrap() -> Any
14 | }
15 |
16 | extension Optional : OptionalProtocol {
17 | func isSome() -> Bool {
18 | switch self {
19 | case .none: return false
20 | case .some: return true
21 | }
22 | }
23 |
24 | func unwrap() -> Any {
25 | switch self {
26 | case .none: preconditionFailure("trying to unwrap nil")
27 | case .some(let unwrapped): return unwrapped
28 | }
29 | }
30 | }
31 |
32 | struct OptionalView: View {
33 |
34 | @Binding var value: Any
35 |
36 | func unwrap(_ value: Any) -> Any {
37 | guard let optional = value as? OptionalProtocol, optional.isSome() else {
38 | return value
39 | }
40 | return optional.unwrap()
41 | }
42 |
43 | func isSome(_ value: Any) -> Bool {
44 | guard let optional = value as? OptionalProtocol else {
45 | return true
46 | }
47 | return optional.isSome()
48 | }
49 |
50 | public var body: some View {
51 | if isSome(value) {
52 | UnknownView(value: Binding(get: { unwrap(value) }, set: { value = $0 }))
53 | } else {
54 | Text("nil")
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Views/PropertyPreview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PropertyPreview.swift
3 | // SwiftGUI
4 | //
5 | // Created by Yonas Kolb on 5/3/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public protocol PropertyPreview {
12 |
13 | associatedtype V: View
14 | associatedtype Value
15 | associatedtype Child: View = EmptyView
16 |
17 | func body(context: PropertyPreviewContext) -> V
18 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig
19 | func child(context: PropertyPreviewContext) -> Child
20 | }
21 |
22 |
23 | public struct PropertyPreviewConfig {
24 |
25 | public var matchType: Bool
26 | public var showPropertyPreview: Bool
27 | public var canNavigate: Bool
28 | public var customView: Bool
29 | public var axis: Axis
30 |
31 | public init(matchType: Bool = true, showPropertyPreview: Bool = true, canNavigate: Bool = false, axis: Axis = .horizontal, customView: Bool = false) {
32 | self.showPropertyPreview = showPropertyPreview
33 | self.canNavigate = canNavigate
34 | self.axis = axis
35 | self.customView = customView
36 | self.matchType = matchType
37 | }
38 | }
39 |
40 | public struct PropertyPreviewContext {
41 | public let property: Property
42 | public let config: Config
43 | public let propertyConfig: PropertyPreviewConfig
44 | public let binding: Binding
45 | public var value: T { binding.wrappedValue }
46 | }
47 |
48 |
49 | extension PropertyPreview {
50 |
51 | func getConfig(property: Property, config: Config) -> PropertyPreviewConfig {
52 | return PropertyPreviewConfig()
53 | }
54 |
55 | func body(context: PropertyPreviewContext) -> some View {
56 | Text(String(describing: context.value)).lineLimit(1)
57 | }
58 | }
59 |
60 | extension PropertyPreview where Child == EmptyView {
61 |
62 | func child(context: PropertyPreviewContext) -> Child {
63 | EmptyView()
64 | }
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Views/SwiftView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftView.swift
3 | // SwiftGUI
4 | //
5 | // Created by Yonas Kolb on 3/3/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Runtime
11 |
12 | /// Put inside a NavigationView
13 | public struct SwiftView: View {
14 |
15 | @Binding var value: Any
16 | private var name: String
17 | let config: Config
18 |
19 | public init(value binding: Binding, config: Config = Config()) {
20 | self._value = binding
21 | self.name = santizedType(of: binding.wrappedValue)
22 | self.config = config
23 | }
24 |
25 | public init(value binding: Binding, config: Config = Config()) {
26 | let anyBinding = binding.map(
27 | get: { $0 as Any },
28 | set: { $0 as! T })
29 | self._value = anyBinding
30 | self.name = santizedType(of: binding.wrappedValue)
31 | self.config = config
32 | }
33 |
34 | public var body: some View {
35 | UnknownView(value: $value, isRoot: true)
36 | .environmentObject(config)
37 | }
38 |
39 | public func showRootNavTitle(_ show: Bool) -> some View {
40 | self.environment(\.showRootNavTitle, false)
41 | }
42 | }
43 |
44 | struct SwiftView_Previews: PreviewProvider {
45 | static var previews: some View {
46 | Group {
47 | NavigationView {
48 | Container(state: TestObject(), config: Config(editing: true))
49 | }
50 | Container(state: "Some text. Some text. Some text. Some text. Some text. Some text. Some text. Some text. ")
51 | }
52 | }
53 |
54 | struct Container: View {
55 | @State var state: S
56 | var config = Config()
57 |
58 | var body: some View {
59 | SwiftView(value: $state, config: config)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/SwiftGUI/Views/UnknownView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnknownView.swift
3 | // SwiftGUI
4 | //
5 | // Created by Yonas Kolb on 4/3/20.
6 | // Copyright © 2020 Yonas. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Runtime
12 |
13 | struct UnknownView: View {
14 |
15 | @Binding var value: Any
16 | private let isRoot: Bool
17 | private var name: String
18 | let typeInfo: TypeInfo
19 | @Environment(\.showRootNavTitle) var showRootNavTitle
20 |
21 | init(value binding: Binding, isRoot: Bool = false) {
22 | self.isRoot = isRoot
23 | let value = binding.wrappedValue
24 | name = santizedType(of: value)
25 | typeInfo = try! Runtime.typeInfo(of: type(of: value))
26 | self._value = binding
27 | }
28 |
29 | public var body: some View {
30 | if !isRoot || showRootNavTitle {
31 | content
32 | #if os(iOS)
33 | .navigationBarTitle(Text(name), displayMode: .inline)
34 | #endif
35 | } else {
36 | content
37 | }
38 | }
39 |
40 | var content: some View {
41 | Group {
42 | if typeInfo.kind == .optional {
43 | OptionalView(value: $value)
44 | } else if isSimpleType(value) {
45 | ScrollView {
46 | Text(String(describing: value))
47 | .padding()
48 | }
49 | } else if typeInfo.kind == .struct || typeInfo.kind == .class {
50 | ObjectView($value, isRoot: isRoot)
51 | } else if typeInfo.kind == .enum {
52 | EnumView($value)
53 | } else {
54 | Text(String(describing: value))
55 | }
56 | }
57 | }
58 | }
59 |
60 | struct RootNavTitleKey: EnvironmentKey {
61 |
62 | static var defaultValue: Bool = true
63 | }
64 |
65 | extension EnvironmentValues {
66 |
67 | public var showRootNavTitle: Bool {
68 | get {
69 | self[RootNavTitleKey.self]
70 | }
71 | set {
72 | self[RootNavTitleKey.self] = newValue
73 | }
74 | }
75 | }
76 |
77 | func isSimpleType(_ value: Any) -> Bool {
78 | switch value {
79 | case is Int: return true
80 | case is Double: return true
81 | case is Bool: return true
82 | case is String: return true
83 | case is URL: return true
84 | default: return false
85 | }
86 | }
87 |
88 | extension View {
89 |
90 | func swiftLink(_ binding: Binding, config: Config) -> some View {
91 | Group {
92 | if isSimpleType(binding.wrappedValue) {
93 | self
94 | } else {
95 | NavigationLink(destination: UnknownView(value: binding).environmentObject(config)) {
96 | self
97 | }
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------