├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── Reflection
│ ├── Reflection.swift
│ ├── String+.swift
│ └── Util.swift
└── ReflectionView
│ ├── Assets
│ └── Media.xcassets
│ │ ├── Contents.json
│ │ └── default
│ │ ├── Contents.json
│ │ ├── constant.colorset
│ │ └── Contents.json
│ │ ├── keyword.colorset
│ │ └── Contents.json
│ │ ├── number.colorset
│ │ └── Contents.json
│ │ ├── string.colorset
│ │ └── Contents.json
│ │ └── type.colorset
│ │ └── Contents.json
│ ├── Component
│ └── ItemCountLabel.swift
│ ├── Config.swift
│ ├── ContentView
│ ├── DictContent.swift
│ ├── EnumCaseContent.swift
│ ├── KeyedContent.swift
│ ├── ListContent.swift
│ ├── NestedContent.swift
│ └── TypedContent.swift
│ ├── Extension
│ └── View+.swift
│ ├── ReflectionContentView.swift
│ ├── ReflectionView.swift
│ ├── TypeInfoContentView.swift
│ └── TypeInfoView.swift
└── Tests
└── ReflectionViewTests
└── ReflectionViewTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 p-x9
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-magic-mirror",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/p-x9/swift-magic-mirror.git",
7 | "state" : {
8 | "revision" : "38735008508388fff40bff3ba690174e779ece7c",
9 | "version" : "0.1.0"
10 | }
11 | },
12 | {
13 | "identity" : "swiftuicolor",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/p-x9/SwiftUIColor.git",
16 | "state" : {
17 | "revision" : "cfea559bcbd681899a8b815e8e84eaadd8be8c2e",
18 | "version" : "0.6.0"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "ReflectionView",
7 | platforms: [
8 | .iOS(.v13),
9 | .macOS(.v10_15)
10 | ],
11 | products: [
12 | .library(
13 | name: "ReflectionView",
14 | targets: ["ReflectionView"]
15 | ),
16 | ],
17 | dependencies: [
18 | .package(url: "https://github.com/p-x9/SwiftUIColor.git", from: "0.6.0"),
19 | .package(url: "https://github.com/p-x9/swift-magic-mirror.git", from: "0.1.0")
20 | ],
21 | targets: [
22 | .target(
23 | name: "Reflection",
24 | dependencies: [
25 | .product(name: "MagicMirror", package: "swift-magic-mirror")
26 | ]
27 | ),
28 | .target(
29 | name: "ReflectionView",
30 | dependencies: [
31 | "Reflection",
32 | .product(name: "SwiftUIColor", package: "SwiftUIColor"),
33 | .product(name: "MagicMirror", package: "swift-magic-mirror")
34 | ],
35 | resources: [
36 | .process("Assets")
37 | ]
38 | ),
39 | .testTarget(
40 | name: "ReflectionViewTests",
41 | dependencies: [
42 | "Reflection",
43 | "ReflectionView"
44 | ]
45 | ),
46 | ]
47 | )
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReflectionView
2 |
3 | SwiftUI View to display property information based on Swift's reflection API for any type of value.
4 |
5 |
6 |
7 | [](https://github.com/p-x9/swiftui-reflection-view/issues)
8 | [](https://github.com/p-x9/swiftui-reflection-view/network/members)
9 | [](https://github.com/p-x9/swiftui-reflection-view/stargazers)
10 | [](https://github.com/p-x9/swiftui-reflection-view/)
11 |
12 | ## Demo
13 |
14 | | A | B | C |
15 | | ---- | ---- | ---- |
16 | |
|
|
|
17 |
18 | ## Usage
19 |
20 | ```swift
21 | let value = Item()
22 |
23 | ReflectionView(value)
24 | ```
25 |
26 | ## License
27 |
28 | ReflectionView is released under the MIT License. See [LICENSE](./LICENSE)
29 |
--------------------------------------------------------------------------------
/Sources/Reflection/Reflection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Reflection.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/02/07.
6 | //
7 | //
8 |
9 | import Foundation
10 | import MagicMirror
11 |
12 | public struct Reflection {
13 | public let value: Any?
14 | public let mirror: MagicMirror?
15 |
16 | public var structured: Element {
17 | parse()
18 | }
19 |
20 | public init(_ value: Any?) {
21 | self.value = value
22 | if let value {
23 | self.mirror = .init(reflecting: value)
24 | } else {
25 | self.mirror = nil
26 | }
27 | }
28 | }
29 |
30 | extension Reflection: CustomStringConvertible {
31 | public var description: String {
32 | structured.description
33 | }
34 |
35 | public var typeDescription: String {
36 | structured.typeDescription
37 | }
38 | }
39 |
40 | extension Reflection {
41 | public indirect enum Element {
42 | case `nil`
43 | case string(String)
44 | case hex(any FixedWidthInteger)
45 | case number(any Numeric)
46 | case bool(Bool)
47 |
48 | case type(Any.Type)
49 |
50 | case list([Element])
51 | case dict([(Element, Element)])
52 |
53 | case enumCase(String, [Element])
54 |
55 | case nested([Element])
56 |
57 | case typed(type: Any.Type, element: Element)
58 | case keyed(key: String, element: Element)
59 | }
60 | }
61 |
62 | extension Reflection.Element: CustomStringConvertible {
63 | private var tabWidth: Int { 4 }
64 |
65 | public var description: String {
66 | switch self {
67 | case .nil:
68 | return "nil"
69 |
70 | case let .string(v):
71 | return "\"\(v)\""
72 |
73 | case let .hex(v):
74 | return "0x" + String(v, radix: 16)
75 |
76 | case let .number(v):
77 | return "\(v)"
78 |
79 | case let .bool(v):
80 | return "\(v)"
81 |
82 | case let .type(type):
83 | return String(reflecting: type).strippedSwiftModulePrefix.strippedCModulePrefix.replacedToCommonSyntaxSugar + ".self"
84 |
85 | case let .list(elements):
86 | if elements.isEmpty { return "[]" }
87 | return """
88 | [
89 | \(elements.map({ $0.description.tabbed(tabWidth) }).joined(separator: "\n"))
90 | ]
91 | """
92 |
93 | case let .dict(elements):
94 | if elements.isEmpty { return "[:]" }
95 | return """
96 | [
97 | \(elements.map({ $0.0.description.tabbed(tabWidth) + ":\n" + $0.1.description.tabbed(2 * tabWidth) }).joined(separator: "\n"))
98 | ]
99 | """
100 |
101 | case let .enumCase(label, values):
102 | if values.isEmpty { return ".\(label)" }
103 | if canDisplayOneline {
104 | return ".\(label)(\(values.map(\.description).joined(separator: ", ")))"
105 | }
106 | return """
107 | .\(label)(
108 | \(values.map({ $0.description.tabbed(tabWidth) }).joined(separator: "\n"))
109 | )
110 | """
111 |
112 | case let .nested(elements):
113 | if elements.isEmpty { return "{}" }
114 | return """
115 | {
116 | \(elements.map({ $0.description.tabbed(tabWidth) }).joined(separator: "\n"))
117 | }
118 | """
119 |
120 | case let .typed(type, element):
121 | return "<\(shorthandName(of: type))> \(element)"
122 |
123 | case let .keyed(key, element):
124 | return "\(key): \(element)"
125 | }
126 | }
127 | }
128 |
129 | extension Reflection {
130 | private func parse(omitRootType: Bool = false) -> Element {
131 | guard let mirror,
132 | let value else {
133 | return .nil
134 | }
135 |
136 | let type = mirror.subjectType
137 | let typeName: String = name(of: type)
138 |
139 | var element: Element?
140 |
141 | switch value {
142 | case let v as Optional where typeName.hasPrefix("Optional<"):
143 | if case let .some(wrapped) = v {
144 | let element = Reflection(wrapped).parse()
145 | if case let.typed(type, element) = element {
146 | return .typed(type: optionalType(of: type), element: element)
147 | }
148 | return element
149 | } else {
150 | element = .nil
151 | }
152 | case let v as any FixedWidthInteger:
153 | element = .hex(v)
154 | case let v as any Numeric:
155 | element = .number(v)
156 | case let v as String:
157 | element = .string(v)
158 | case let v as Bool:
159 | element = .bool(v)
160 | case let v as Dictionary:
161 | let elements = v.map {
162 | (Reflection($0.key.base).parse(), Reflection($0.value).parse())
163 | }
164 | element = .dict(elements)
165 | case let v as Array:
166 | let elements = v.map {
167 | Reflection($0).parse()
168 | }
169 | element = .list(elements)
170 | case let v as Any.Type:
171 | element = .type(v)
172 | case let v as NSNumber:
173 | element = .number(v.decimalValue)
174 | default: break
175 | }
176 |
177 | if let element {
178 | if omitRootType {
179 | return element
180 | } else {
181 | return .typed(type: type, element: element)
182 | }
183 | }
184 |
185 | if mirror.displayStyle == .enum {
186 | let enumCase: Element = {
187 | if let (label, tuple) = mirror.children.first {
188 | let mirror = MagicMirror(reflecting: tuple)
189 | if mirror.displayStyle == .tuple {
190 | let values: [Element] = mirror
191 | .children
192 | .map {
193 | let element = Reflection($0.value).parse()
194 | if let label = $0.label {
195 | return .keyed(key: label, element: element)
196 | }
197 | return element
198 | }
199 | return .enumCase(label ?? "", values)
200 | } else {
201 | return .enumCase(label ?? "", [Reflection(tuple).parse()])
202 | }
203 | } else {
204 | return .enumCase("\(value)", [])
205 | }
206 | }()
207 |
208 | if omitRootType {
209 | return enumCase
210 | } else {
211 | return .typed(type: type, element: enumCase)
212 | }
213 | }
214 |
215 | var nestedElements: [Element] = []
216 |
217 | for case let (key?, value) in mirror.children {
218 | let element = Reflection(value).parse()
219 | nestedElements.append(
220 | .keyed(key: key, element: element)
221 | )
222 | }
223 |
224 | if omitRootType {
225 | return .nested(nestedElements)
226 | } else {
227 | return .typed(
228 | type: type,
229 | element: .nested(nestedElements)
230 | )
231 | }
232 | }
233 | }
234 |
235 | extension Reflection.Element {
236 | public var typeDescription: String {
237 | switch self {
238 | case let .nested(elements):
239 | if elements.isEmpty { return "{}" }
240 | return """
241 | {
242 | \(elements.map({ $0.typeDescription.tabbed(tabWidth) }).joined(separator: "\n"))
243 | }
244 | """
245 |
246 | case let .typed(type, element):
247 | return "\(shorthandName(of: type)) \(element.typeDescription)"
248 |
249 | case let .keyed(key, element):
250 | return "\(key): \(element.typeDescription)"
251 |
252 | default:
253 | return ""
254 | }
255 | }
256 | }
257 |
258 | extension Reflection.Element {
259 | package var canDisplayOneline: Bool {
260 | switch self {
261 | case let .list(elements): elements.isEmpty
262 | case let .dict(pairs): pairs.isEmpty
263 | case let .enumCase(_, values): values.allSatisfy(\.canDisplayOneline)
264 | case let .nested(elements): elements.isEmpty
265 | case let .typed(type: _, element: element): element.canDisplayOneline
266 | case let .keyed(key: _, element: element): element.canDisplayOneline
267 | default: true
268 | }
269 | }
270 | }
271 |
272 | extension Sequence where Element == Reflection.Element {
273 | public var isSameType: Bool {
274 | guard let root = first(where: { _ in true }) else {
275 | return true
276 | }
277 | return allSatisfy {
278 | $0.typeDescription == root.typeDescription
279 | }
280 | }
281 | }
282 |
283 | package func name(of type: Any.Type) -> String {
284 | String(reflecting: type)
285 | .strippedSwiftModulePrefix
286 | .strippedCModulePrefix
287 | }
288 |
289 | package func shorthandName(of type: Any.Type) -> String {
290 | name(of: type)
291 | .replacedToCommonSyntaxSugar
292 | }
293 |
--------------------------------------------------------------------------------
/Sources/Reflection/String+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/15.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | extension String {
12 | func tabbed(_ width: Int = 4) -> String {
13 | components(separatedBy: .newlines)
14 | .map { String(repeating: " ", count: width) + $0 }
15 | .joined(separator: "\n")
16 | }
17 | }
18 |
19 | extension String {
20 | package var strippedSwiftModulePrefix: String {
21 | var string = self
22 | if string.starts(with: "Swift.") {
23 | string = String(string.dropFirst(6))
24 | }
25 | return string
26 | .replacingOccurrences(of: "") else {
59 | return self
60 | }
61 |
62 | let startIndex = trailing.index(trailing.startIndex, offsetBy: 1)
63 | let endIndex = trailing.index(trailing.startIndex, offsetBy: closingIndex)
64 | let content = String(trailing[startIndex ..< endIndex]) //
65 | .replacedToOptionalSyntaxSugar
66 | .trimmedLeadingAndTrailingWhitespaces
67 |
68 | let suffix = String(trailing[trailing.index(after: endIndex)...])
69 | .replacedToOptionalSyntaxSugar
70 | .trimmedLeadingAndTrailingWhitespaces
71 |
72 | return prefix + content + "?" + suffix
73 | }
74 |
75 | package var replacedToArraySyntaxSugar: String {
76 | guard let range = range(of: "Array<") else { return self }
77 | let prefix = self[startIndex ..< range.lowerBound]
78 | let trailing = String(self[index(before: range.upperBound)...])
79 |
80 | guard let closingIndex = trailing.indexForMatchingBracket(open: "<", close: ">") else {
81 | return self
82 | }
83 |
84 | let startIndex = trailing.index(trailing.startIndex, offsetBy: 1)
85 | let endIndex = trailing.index(trailing.startIndex, offsetBy: closingIndex)
86 | let content = String(trailing[startIndex ..< endIndex]) //
87 | .replacedToArraySyntaxSugar
88 | .trimmedLeadingAndTrailingWhitespaces
89 |
90 | let suffix = String(trailing[trailing.index(after: endIndex)...])
91 | .replacedToArraySyntaxSugar
92 | .trimmedLeadingAndTrailingWhitespaces
93 |
94 | return prefix + "[" + content + "]" + suffix
95 | }
96 |
97 | package var replacedToDictionarySyntaxSugar: String {
98 | guard let range = range(of: "Dictionary<") else { return self }
99 | let prefix = self[startIndex ..< range.lowerBound]
100 | let trailing = String(self[index(before: range.upperBound)...])
101 |
102 | guard let closingIndex = trailing.indexForMatchingBracket(open: "<", close: ">") else {
103 | return self
104 | }
105 |
106 | let startIndex = trailing.index(trailing.startIndex, offsetBy: 1)
107 | let endIndex = trailing.index(trailing.startIndex, offsetBy: closingIndex)
108 | let content = String(trailing[startIndex ..< endIndex]) //
109 |
110 | let keyAndValue = content.contents(
111 | separatedBy: ",",
112 | openings: ["(", "<", "["],
113 | closings: [")", ">", "]"]
114 | )
115 | guard keyAndValue.count == 2 else { return self }
116 | let key = keyAndValue[0]
117 | .replacedToDictionarySyntaxSugar
118 | .trimmedLeadingAndTrailingWhitespaces
119 | let value = keyAndValue[1]
120 | .replacedToDictionarySyntaxSugar
121 | .trimmedLeadingAndTrailingWhitespaces
122 |
123 | let suffix = String(trailing[trailing.index(after: endIndex)...])
124 | .replacedToDictionarySyntaxSugar
125 | .trimmedLeadingAndTrailingWhitespaces
126 |
127 | return prefix + "[" + key + ": " + value + "]" + suffix
128 | }
129 | }
130 |
131 | extension String {
132 | /// Finds the index of the closing bracket that matches the first encountered opening bracket.
133 | ///
134 | /// This method iterates through the characters of the string and tracks the depth of nested brackets.
135 | /// When the depth returns to zero, the matching closing bracket for the first opening bracket is found.
136 | ///
137 | /// - Parameters:
138 | /// - open: The character representing the opening bracket (e.g., `<`, `(`, `[`)
139 | /// - close: The character representing the closing bracket (e.g., `>`, `)`, `]`)
140 | /// - Returns: The index (0-based) of the matching closing bracket within the string, or `nil` if unmatched.
141 | /// - Complexity: O(n), where n is the length of the string.
142 | func indexForMatchingBracket(
143 | open: Character,
144 | close: Character
145 | ) -> Int? {
146 | var depth = 0
147 | for (index, char) in enumerated() {
148 | depth += (char == open) ? 1 : (char == close) ? -1 : 0
149 | if depth == 0 {
150 | return index
151 | }
152 | }
153 | return nil
154 | }
155 | }
156 |
157 | extension String {
158 | /// Splits the string by a separator character while ignoring separators inside nested delimiters.
159 | ///
160 | /// This method is useful for parsing comma-separated types or parameters where nested brackets may exist.
161 | ///
162 | /// - Parameters:
163 | /// - separator: The character used to split the string (e.g., `,`)
164 | /// - openings: A list of characters considered as opening delimiters (e.g., `(`, `<`, `[`)
165 | /// - closings: A list of characters considered as closing delimiters (e.g., `)`, `>`, `]`)
166 | /// - Returns: An array of substrings split on the separator, ignoring separators inside nested delimiters.
167 | func contents(
168 | separatedBy separator: Character,
169 | openings: [Character] = [],
170 | closings: [Character] = []
171 | ) -> [String] {
172 | var result: [String] = []
173 | var depth = 0
174 | var entry: [Character] = []
175 |
176 | for (_, char) in enumerated() {
177 | depth += (openings.contains(char)) ? 1 : (closings.contains(char)) ? -1 : 0
178 | if depth == 0 && char == separator {
179 | result.append(String(entry))
180 | entry = []
181 | } else {
182 | entry.append(char)
183 | }
184 | }
185 | return result + [String(entry)]
186 | }
187 | }
188 |
189 | extension String {
190 | /// Trims leading and trailing whitespaces from the string.
191 | ///
192 | /// This method uses a regular expression to remove spaces from both the beginning and end of the string.
193 | /// It does not affect whitespaces within the string body.
194 | ///
195 | /// - Returns: A new string with surrounding whitespaces removed.
196 | var trimmedLeadingAndTrailingWhitespaces: String {
197 | let pattern = #"^\s+|\s+$"#
198 | if let regex = try? NSRegularExpression(pattern: pattern) {
199 | let range = NSRange(location: 0, length: utf16.count)
200 | let trimmed = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "")
201 | return trimmed
202 | }
203 | return self
204 | /* replacing(/^\s+|\s+$/, with: "") */
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/Sources/Reflection/Util.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Util.swift
3 | // ReflectionView
4 | //
5 | // Created by p-x9 on 2025/05/31
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | func optionalType(of type: Any.Type) -> Any.Type {
12 | _openExistential(type, do: _optionalType(_:))
13 | }
14 |
15 | func _optionalType(_ type: T.Type) -> Any.Type {
16 | Optional.self
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Assets/Media.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Assets/Media.xcassets/default/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | },
6 | "properties" : {
7 | "provides-namespace" : true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Assets/Media.xcassets/default/constant.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xB2",
9 | "green" : "0x51",
10 | "red" : "0x78"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "display-p3",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xDC",
27 | "green" : "0x7F",
28 | "red" : "0xA4"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Assets/Media.xcassets/default/keyword.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x9F",
9 | "green" : "0x45",
10 | "red" : "0x9F"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "display-p3",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xB0",
27 | "green" : "0x81",
28 | "red" : "0xED"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Assets/Media.xcassets/default/number.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xCF",
9 | "green" : "0x29",
10 | "red" : "0x27"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "display-p3",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x85",
27 | "green" : "0xC9",
28 | "red" : "0xD5"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Assets/Media.xcassets/default/string.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x29",
9 | "green" : "0x3E",
10 | "red" : "0xC0"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "display-p3",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x75",
27 | "green" : "0x87",
28 | "red" : "0xEE"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Assets/Media.xcassets/default/type.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xAF",
9 | "green" : "0x35",
10 | "red" : "0x53"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "display-p3",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xFA",
27 | "green" : "0xBB",
28 | "red" : "0xD5"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Component/ItemCountLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItemCountLabel..swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import SwiftUIColor
12 |
13 | struct ItemCountLabel: View {
14 | let count: Int
15 |
16 | init(_ count: Int) {
17 | self.count = count
18 | }
19 |
20 | var body: some View {
21 | Text("\(count) items")
22 | .foregroundColor(.universal(.systemGray))
23 | .scaleEffect(0.8)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Config.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Config.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/15.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | public struct Config {
13 | public var keywordColor: Color
14 | public var stringColor: Color
15 | public var numberColor: Color
16 | public var typeColor: Color
17 | public var constantColor: Color
18 | public var itemLimitForExpansion = 5
19 |
20 | public init(
21 | keywordColor: Color,
22 | stringColor: Color,
23 | numberColor: Color,
24 | typeColor: Color,
25 | constantColor: Color,
26 | itemLimitForExpansion: Int = 5
27 | ) {
28 | self.keywordColor = keywordColor
29 | self.stringColor = stringColor
30 | self.numberColor = numberColor
31 | self.typeColor = typeColor
32 | self.constantColor = constantColor
33 | self.itemLimitForExpansion = itemLimitForExpansion
34 | }
35 | }
36 |
37 | extension Config {
38 | public static var `default`: Config {
39 | .init(
40 | keywordColor: .init("default/keyword", bundle: .module),
41 | stringColor: .init("default/string", bundle: .module),
42 | numberColor: .init("default/number", bundle: .module),
43 | typeColor: .init("default/type", bundle: .module),
44 | constantColor: .init("default/constant", bundle: .module)
45 | )
46 | }
47 | }
48 |
49 | public struct ReflectionViewConfigKey: EnvironmentKey {
50 | public typealias Value = Config
51 |
52 | public static var defaultValue: Config {
53 | .default
54 | }
55 | }
56 |
57 | extension EnvironmentValues {
58 | public var reflectionViewConfig: Config {
59 | get {
60 | self[ReflectionViewConfigKey.self]
61 | }
62 | set {
63 | self[ReflectionViewConfigKey.self] = newValue
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/ContentView/DictContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DictContent.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/15.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Reflection
12 |
13 | struct DictContent: View {
14 | let type: String?
15 | let key: String?
16 | let elements: [(key: Reflection.Element, value: Reflection.Element)]
17 | let showTypeInfoOnly: Bool
18 |
19 | @State var isExpanded = false
20 | @Environment(\.reflectionViewConfig) var config
21 |
22 | init(
23 | type: String?,
24 | key: String?,
25 | _ elements: [(Reflection.Element, Reflection.Element)],
26 | showTypeInfoOnly: Bool = false,
27 | isExpanded: Bool = false
28 | ) {
29 | self.type = type
30 | self.key = key
31 | self.elements = elements
32 | self.showTypeInfoOnly = showTypeInfoOnly
33 | self._isExpanded = .init(initialValue: isExpanded)
34 | }
35 |
36 | var body: some View {
37 | if elements.isEmpty || (showTypeInfoOnly && !elements.canShowChildTypeInfo) {
38 | emptyView
39 | } else {
40 | VStack {
41 | HStack(spacing: 2) {
42 | if let key {
43 | Text("\(key):")
44 | }
45 | if let type {
46 | Text("\(type)")
47 | .foregroundColor(config.typeColor)
48 | }
49 | leftSquare
50 | if !isExpanded {
51 | Text("...")
52 | .background(Color.universal(.systemGray).opacity(0.3))
53 | .cornerRadius(3.0)
54 | rightSquare
55 | ItemCountLabel(showTypeInfoOnly ? 1 : elements.count)
56 | }
57 | }
58 | .frame(maxWidth: .infinity, alignment: .leading)
59 | .onTapGesture { withAnimation { isExpanded.toggle() } }
60 | if isExpanded {
61 | elementsView
62 | HStack {
63 | rightSquare
64 | Spacer()
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
71 | @ViewBuilder
72 | var elementsView: some View {
73 | let keys = elements.map(\.key)
74 | let values = elements.map(\.value)
75 | if showTypeInfoOnly,
76 | let key = keys.first, let value = values.first {
77 | VStack {
78 | HStack(spacing: 0) {
79 | TypeInfoContentView(key)
80 | .padding(.leading, 16)
81 | colon
82 | Spacer()
83 | }
84 | .frame(maxWidth: .infinity, alignment: .leading)
85 | TypeInfoContentView(value)
86 | .frame(maxWidth: .infinity, alignment: .leading)
87 | .padding(.leading, 32)
88 | }
89 | } else {
90 | VStack {
91 | ForEach(elements.indices, id: \.self) { index in
92 | VStack {
93 | HStack(spacing: 0) {
94 | ReflectionContentView(elements[index].key)
95 | .padding(.leading, 16)
96 | colon
97 | Spacer()
98 | }
99 | .frame(maxWidth: .infinity, alignment: .leading)
100 | ReflectionContentView(elements[index].value)
101 | .frame(maxWidth: .infinity, alignment: .leading)
102 | .padding(.leading, 32)
103 | }
104 | }
105 | }
106 | }
107 | }
108 |
109 | var leftSquare: some View {
110 | Text("[")
111 | .foregroundColor(.universal(.systemGray))
112 | }
113 |
114 | var rightSquare: some View {
115 | Text("]")
116 | .foregroundColor(.universal(.systemGray))
117 | }
118 |
119 | var colon: some View {
120 | Text(":")
121 | .foregroundColor(.universal(.systemGray))
122 | }
123 |
124 | var emptyView: some View {
125 | HStack(spacing: 2) {
126 | if let key {
127 | Text("\(key):")
128 | }
129 | if let type {
130 | Text(type)
131 | .foregroundColor(config.typeColor)
132 | }
133 | if !showTypeInfoOnly {
134 | Text("[:]")
135 | .foregroundColor(.universal(.systemGray))
136 | }
137 | }
138 | }
139 | }
140 |
141 | extension [(key: Reflection.Element, value: Reflection.Element)] {
142 | fileprivate var canShowChildTypeInfo: Bool {
143 | guard !isEmpty else { return false }
144 | let keys = map(\.key)
145 | let values = map(\.value)
146 | return keys.isSameType && values.isSameType
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/ContentView/EnumCaseContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnumCaseContent.swift
3 | // ReflectionView
4 | //
5 | // Created by p-x9 on 2025/05/30
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Reflection
12 |
13 | struct EnumCaseContent: View {
14 | let type: String?
15 | let key: String?
16 | let name: String
17 | let values: [Reflection.Element]
18 | let canDisplayOneline: Bool
19 | let showTypeInfoOnly: Bool
20 |
21 | @State var isExpanded = false
22 | @Environment(\.reflectionViewConfig) var config
23 |
24 | init(
25 | type: String?,
26 | key: String?,
27 | name: String,
28 | _ values: [Reflection.Element],
29 | canDisplayOneline: Bool,
30 | showTypeInfoOnly: Bool = false,
31 | isExpanded: Bool = false
32 | ) {
33 | self.type = type
34 | self.key = key
35 | self.name = name
36 | self.values = values
37 | self.canDisplayOneline = canDisplayOneline
38 | self.showTypeInfoOnly = showTypeInfoOnly
39 | self._isExpanded = .init(initialValue: isExpanded)
40 | }
41 |
42 | var body: some View {
43 | if values.isEmpty {
44 | simpleCaseView
45 | } else {
46 | VStack {
47 | HStack(spacing: 0) {
48 | if let key {
49 | Text("\(key):")
50 | .padding(.trailing, 2)
51 | }
52 | if let type {
53 | Text("\(type)")
54 | .foregroundColor(config.typeColor)
55 | }
56 | Text("." + name)
57 | .foregroundColor(config.constantColor)
58 | leftParen
59 | if !isExpanded && !canDisplayOneline {
60 | Text("...")
61 | .background(Color.universal(.systemGray).opacity(0.3))
62 | .cornerRadius(3.0)
63 | rightParen
64 | ItemCountLabel(values.count)
65 | }
66 | if canDisplayOneline {
67 | valuesView
68 | rightParen
69 | }
70 | }
71 | .frame(maxWidth: .infinity, alignment: .leading)
72 | .onTapGesture { withAnimation { isExpanded.toggle() } }
73 |
74 | if isExpanded && !canDisplayOneline {
75 | valuesView
76 | HStack {
77 | rightParen
78 | Spacer()
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
85 | @ViewBuilder
86 | var valuesView: some View {
87 | if canDisplayOneline {
88 | HStack {
89 | _valuesView
90 | }
91 | } else {
92 | VStack {
93 | _valuesView
94 | .frame(maxWidth: .infinity, alignment: .leading)
95 | .padding(.leading, 16)
96 | }
97 | }
98 | }
99 |
100 | var _valuesView: some View {
101 | ForEach(values.indices, id: \.self) { index in
102 | if showTypeInfoOnly{
103 | TypeInfoContentView(values[index])
104 | } else {
105 | ReflectionContentView(values[index])
106 | }
107 | }
108 | }
109 |
110 | var leftParen: some View {
111 | Text("(")
112 | .foregroundColor(.universal(.systemGray))
113 | }
114 |
115 | var rightParen: some View {
116 | Text(")")
117 | .foregroundColor(.universal(.systemGray))
118 | }
119 |
120 | var simpleCaseView: some View {
121 | HStack(spacing: 0) {
122 | if let key {
123 | Text("\(key):")
124 | .padding(.trailing, 2)
125 | }
126 | if let type {
127 | Text("\(type)")
128 | .foregroundColor(config.typeColor)
129 | }
130 | Text("." + name)
131 | .foregroundColor(config.constantColor)
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/ContentView/KeyedContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyedContent.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/15.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct KeyedContent: View {
13 | let key: String
14 | let content: Content
15 |
16 | init(_ key: String, @ViewBuilder content: () -> Content) {
17 | self.key = key
18 | self.content = content()
19 | }
20 |
21 | var body: some View {
22 | HStack(alignment: .top, spacing: 0) {
23 | Text(key)
24 | Text(": ")
25 | content
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/ContentView/ListContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListContent.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/15.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Reflection
12 |
13 | struct ListContent: View {
14 | let type: String?
15 | let key: String?
16 | let elements: [Reflection.Element]
17 | let showTypeInfoOnly: Bool
18 |
19 | @State var isExpanded = false
20 | @Environment(\.reflectionViewConfig) var config
21 |
22 | init(
23 | type: String?,
24 | key: String?,
25 | _ elements: [Reflection.Element],
26 | showTypeInfoOnly: Bool = false,
27 | isExpanded: Bool = false
28 | ) {
29 | self.type = type
30 | self.key = key
31 | self.elements = elements
32 | self.showTypeInfoOnly = showTypeInfoOnly
33 | self._isExpanded = .init(initialValue: isExpanded)
34 | }
35 |
36 | var body: some View {
37 | if elements.isEmpty || (showTypeInfoOnly && !elements.canShowChildTypeInfo) {
38 | emptyView
39 | } else {
40 | VStack {
41 | HStack(spacing: 2) {
42 | if let key {
43 | Text("\(key):")
44 | }
45 | if let type {
46 | Text("\(type)")
47 | .foregroundColor(config.typeColor)
48 | }
49 | leftSquare
50 | if !isExpanded {
51 | Text("...")
52 | .background(Color.universal(.systemGray).opacity(0.3))
53 | .cornerRadius(3.0)
54 | rightSquare
55 | ItemCountLabel(showTypeInfoOnly ? 1 : elements.count)
56 | }
57 | }
58 | .frame(maxWidth: .infinity, alignment: .leading)
59 | .onTapGesture { withAnimation { isExpanded.toggle() } }
60 | if isExpanded {
61 | elementsView
62 | HStack {
63 | rightSquare
64 | Spacer()
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
71 | @ViewBuilder
72 | var elementsView: some View {
73 | if showTypeInfoOnly,
74 | let element = elements.first {
75 | TypeInfoContentView(element)
76 | .frame(maxWidth: .infinity, alignment: .leading)
77 | .padding(.leading, 16)
78 | } else {
79 | VStack {
80 | ForEach(elements.indices, id: \.self) { index in
81 | ReflectionContentView(elements[index])
82 | .frame(maxWidth: .infinity, alignment: .leading)
83 | .padding(.leading, 16)
84 | }
85 | }
86 | }
87 | }
88 |
89 | var leftSquare: some View {
90 | Text("[")
91 | .foregroundColor(.universal(.systemGray))
92 | }
93 |
94 | var rightSquare: some View {
95 | Text("]")
96 | .foregroundColor(.universal(.systemGray))
97 | }
98 |
99 | var emptyView: some View {
100 | HStack(spacing: 2) {
101 | if let key {
102 | Text("\(key):")
103 | }
104 | if let type {
105 | Text("\(type)")
106 | .foregroundColor(config.typeColor)
107 | }
108 | if !showTypeInfoOnly {
109 | Text("[]")
110 | .foregroundColor(.universal(.systemGray))
111 | }
112 | }
113 | }
114 | }
115 |
116 | extension [Reflection.Element] {
117 | fileprivate var canShowChildTypeInfo: Bool {
118 | isSameType
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/ContentView/NestedContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NestedContent.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/15.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Reflection
12 |
13 | struct NestedContent: View {
14 | let type: String?
15 | let key: String?
16 | let elements: [Reflection.Element]
17 | let showTypeInfoOnly: Bool
18 |
19 | @State var isExpanded = false
20 | @Environment(\.reflectionViewConfig) var config
21 |
22 | init(
23 | type: String?,
24 | key: String?,
25 | _ elements: [Reflection.Element],
26 | showTypeInfoOnly: Bool = false,
27 | isExpanded: Bool = false
28 | ) {
29 | self.type = type
30 | self.key = key
31 | self.elements = elements
32 | self.showTypeInfoOnly = showTypeInfoOnly
33 | self._isExpanded = .init(initialValue: isExpanded)
34 | }
35 |
36 | var body: some View {
37 | if elements.isEmpty {
38 | emptyView
39 | } else {
40 | VStack {
41 | HStack(spacing: 2) {
42 | if let key {
43 | Text("\(key):")
44 | }
45 | if let type {
46 | Text("\(type)")
47 | .foregroundColor(config.typeColor)
48 | }
49 | leftBrace
50 | if !isExpanded {
51 | Text("...")
52 | .background(Color.universal(.systemGray).opacity(0.3))
53 | .cornerRadius(3.0)
54 | rightBrace
55 | ItemCountLabel(elements.count)
56 | }
57 | }
58 | .frame(maxWidth: .infinity, alignment: .leading)
59 | .onTapGesture { withAnimation { isExpanded.toggle() } }
60 |
61 | if isExpanded {
62 | elementsView
63 | HStack {
64 | rightBrace
65 | Spacer()
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
72 | var elementsView: some View {
73 | VStack {
74 | ForEach(elements.indices, id: \.self) { index in
75 | if showTypeInfoOnly{
76 | TypeInfoContentView(elements[index])
77 | .frame(maxWidth: .infinity, alignment: .leading)
78 | .padding(.leading, 16)
79 | } else {
80 | ReflectionContentView(elements[index])
81 | .frame(maxWidth: .infinity, alignment: .leading)
82 | .padding(.leading, 16)
83 | }
84 | }
85 | }
86 | }
87 |
88 | var leftBrace: some View {
89 | Text("{")
90 | .foregroundColor(.universal(.systemGray))
91 | }
92 |
93 | var rightBrace: some View {
94 | Text("}")
95 | .foregroundColor(.universal(.systemGray))
96 | }
97 |
98 | var emptyView: some View {
99 | HStack(spacing: 2) {
100 | if let key {
101 | Text("\(key):")
102 | }
103 | if let type {
104 | Text("\(type)")
105 | .foregroundColor(config.typeColor)
106 | }
107 | Text("{}")
108 | .foregroundColor(.universal(.systemGray))
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/ContentView/TypedContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TypedContent.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/15.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | struct TypedContent: View {
13 | let type: String
14 | let content: Content
15 |
16 | @Environment(\.reflectionViewConfig) var config
17 |
18 | init(_ type: String, @ViewBuilder content: () -> Content) {
19 | self.type = type
20 | self.content = content()
21 | }
22 |
23 | var body: some View {
24 | HStack(alignment: .top, spacing: 0) {
25 | Text(type)
26 | .foregroundColor(config.typeColor)
27 | Text(" ")
28 | content
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/Extension/View+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/15.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | extension View {
13 | public func reflectionConfig(_ config: Config) -> some View {
14 | self.environment(\.reflectionViewConfig, config)
15 | }
16 |
17 | public func reflectionItemLimit(_ limit: Int) -> some View {
18 | self.environment(\.reflectionViewConfig.itemLimitForExpansion, limit)
19 | }
20 | }
21 |
22 | extension View {
23 | func set(_ value: T, for keyPath: WritableKeyPath) -> Self {
24 | var new = self
25 | new[keyPath: keyPath] = value
26 | return new
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/ReflectionContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReflectionContentView.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/15.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Reflection
12 |
13 | struct ReflectionContentView: View {
14 | let element: Reflection.Element
15 | let isRoot: Bool
16 | @Environment(\.reflectionViewConfig) var config
17 |
18 | @Environment(\.reflectionViewConfig.itemLimitForExpansion)
19 | var itemLimitForExpansion
20 |
21 | init(_ element: Reflection.Element, isRoot: Bool = false) {
22 | self.element = element
23 | self.isRoot = isRoot
24 | }
25 |
26 | var body: some View {
27 | switch element {
28 | case .nil:
29 | Text("nil")
30 | .foregroundColor(config.keywordColor)
31 |
32 | case let .string(v):
33 | Text("\"\(v)\"")
34 | .foregroundColor(config.stringColor)
35 |
36 | case let .hex(v):
37 | Text("0x" + String(v, radix: 16))
38 | .foregroundColor(config.numberColor)
39 |
40 | case let .number(v):
41 | let string = "\(v)"
42 | Text(string)
43 | .foregroundColor(config.numberColor)
44 |
45 | case let .bool(v):
46 | Text(v.description)
47 | .foregroundColor(config.keywordColor)
48 |
49 | case let .type(v):
50 | HStack(spacing: 0) {
51 | Text(typeName(of: v))
52 | .foregroundColor(config.typeColor)
53 | Text(".")
54 | Text("self")
55 | .foregroundColor(config.keywordColor)
56 | }
57 |
58 | case let .list(elements):
59 | ListContent(
60 | type: nil,
61 | key: nil,
62 | elements,
63 | isExpanded: elements.count <= itemLimitForExpansion
64 | )
65 |
66 | case let .dict(elements):
67 | DictContent(
68 | type: nil,
69 | key: nil,
70 | elements,
71 | isExpanded: elements.count <= itemLimitForExpansion
72 | )
73 |
74 | case let .enumCase(name, values):
75 | EnumCaseContent(
76 | type: nil,
77 | key: nil,
78 | name: name,
79 | values,
80 | canDisplayOneline: element.canDisplayOneline,
81 | isExpanded: values.count <= itemLimitForExpansion
82 | )
83 |
84 | case let .nested(elements):
85 | NestedContent(
86 | type: nil,
87 | key: nil,
88 | elements,
89 | isExpanded: elements.count <= itemLimitForExpansion
90 | )
91 |
92 | case let .keyed(key, element: .list(elements)):
93 | ListContent(
94 | type: nil,
95 | key: key,
96 | elements,
97 | isExpanded: elements.count <= itemLimitForExpansion
98 | )
99 |
100 | case let .keyed(key, element: .dict(elements)):
101 | DictContent(
102 | type: nil,
103 | key: key,
104 | elements,
105 | isExpanded: elements.count <= itemLimitForExpansion
106 | )
107 |
108 | case let .keyed(key, element: .enumCase(name, values)):
109 | EnumCaseContent(
110 | type: nil,
111 | key: key,
112 | name: name,
113 | values,
114 | canDisplayOneline: element.canDisplayOneline,
115 | isExpanded: values.count <= itemLimitForExpansion
116 | )
117 |
118 | case let .keyed(key, element: .nested(elements)):
119 | NestedContent(
120 | type: nil,
121 | key: key,
122 | elements,
123 | isExpanded: elements.count <= itemLimitForExpansion
124 | )
125 |
126 | case let .keyed(key, element: .typed(type, .nested(elements))):
127 | NestedContent(
128 | type: typeName(of: type),
129 | key: key,
130 | elements,
131 | isExpanded: elements.count <= itemLimitForExpansion
132 | )
133 |
134 | case let .keyed(key, element: .typed(type, .list(elements))):
135 | ListContent(
136 | type: typeName(of: type),
137 | key: key,
138 | elements,
139 | isExpanded: elements.count <= itemLimitForExpansion
140 | )
141 |
142 | case let .keyed(key, element: .typed(type, .dict(elements))):
143 | DictContent(
144 | type: typeName(of: type),
145 | key: key,
146 | elements,
147 | isExpanded: elements.count <= itemLimitForExpansion
148 | )
149 |
150 | case let .keyed(key, element: .typed(type, .enumCase(name, values))):
151 | EnumCaseContent(
152 | type: typeName(of: type),
153 | key: key,
154 | name: name,
155 | values,
156 | canDisplayOneline: element.canDisplayOneline,
157 | isExpanded: values.count <= itemLimitForExpansion
158 | )
159 |
160 | case let .typed(type, element: .nested(elements)):
161 | NestedContent(
162 | type: typeName(of: type),
163 | key: nil,
164 | elements,
165 | isExpanded: elements.count <= itemLimitForExpansion
166 | )
167 |
168 | case let .typed(type, element: .list(elements)):
169 | ListContent(
170 | type: typeName(of: type),
171 | key: nil,
172 | elements,
173 | isExpanded: elements.count <= itemLimitForExpansion
174 | )
175 |
176 | case let .typed(type, element: .dict(elements)):
177 | DictContent(
178 | type: typeName(of: type),
179 | key: nil,
180 | elements,
181 | isExpanded: elements.count <= itemLimitForExpansion
182 | )
183 |
184 | case let .typed(type, element: .enumCase(name, values)):
185 | EnumCaseContent(
186 | type: typeName(of: type),
187 | key: nil,
188 | name: name,
189 | values,
190 | canDisplayOneline: element.canDisplayOneline,
191 | isExpanded: values.count <= itemLimitForExpansion
192 | )
193 |
194 | case let .typed(type, element):
195 | TypedContent(typeName(of: type)) {
196 | ReflectionContentView(element)
197 | }
198 |
199 | case let .keyed(key, element):
200 | KeyedContent(key) {
201 | ReflectionContentView(element)
202 | }
203 | }
204 | }
205 | }
206 |
207 | extension ReflectionContentView {
208 | func typeName(of type: Any.Type) -> String {
209 | if isRoot {
210 | name(of: type)
211 | .replacedToOptionalSyntaxSugar
212 | } else {
213 | shorthandName(of: type)
214 | }
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/ReflectionView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftUIColor
3 | import Reflection
4 |
5 | public struct ReflectionView: View {
6 | let reflection: Reflection
7 |
8 | @Environment(\.font) var font
9 | @Environment(\.reflectionViewConfig) var config
10 |
11 | public init(_ value: Any) {
12 | self.reflection = .init(value)
13 | }
14 |
15 | public var body: some View {
16 | let structured = reflection.structured
17 | HStack {
18 | ReflectionContentView(structured, isRoot: true)
19 | }
20 | .padding()
21 | }
22 | }
23 |
24 | #Preview {
25 | let value = Text("hello")
26 | return ScrollView {
27 | ReflectionView(value)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/TypeInfoContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TypeInfoContentView.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/30.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | import Reflection
13 |
14 | struct TypeInfoContentView: View {
15 | let element: Reflection.Element
16 | @Environment(\.reflectionViewConfig) var config
17 |
18 | @Environment(\.reflectionViewConfig.itemLimitForExpansion)
19 | var itemLimitForExpansion
20 |
21 | init(_ element: Reflection.Element) {
22 | self.element = element
23 | }
24 |
25 | var body: some View {
26 | switch element {
27 | case let .list(elements):
28 | ListContent(
29 | type: nil,
30 | key: nil,
31 | elements,
32 | showTypeInfoOnly: true,
33 | isExpanded: elements.count <= itemLimitForExpansion
34 | )
35 |
36 | case let .dict(elements):
37 | DictContent(
38 | type: nil,
39 | key: nil,
40 | elements,
41 | showTypeInfoOnly: true,
42 | isExpanded: elements.count <= itemLimitForExpansion
43 | )
44 |
45 | case let .nested(elements):
46 | NestedContent(
47 | type: nil,
48 | key: nil,
49 | elements,
50 | showTypeInfoOnly: true,
51 | isExpanded: elements.count <= itemLimitForExpansion
52 | )
53 |
54 | case let .keyed(key, element: .list(elements)):
55 | ListContent(
56 | type: nil,
57 | key: key,
58 | elements,
59 | showTypeInfoOnly: true,
60 | isExpanded: elements.count <= itemLimitForExpansion
61 | )
62 |
63 | case let .keyed(key, element: .dict(elements)):
64 | DictContent(
65 | type: nil,
66 | key: key,
67 | elements,
68 | showTypeInfoOnly: true,
69 | isExpanded: elements.count <= itemLimitForExpansion
70 | )
71 |
72 | case let .keyed(key, element: .nested(elements)):
73 | NestedContent(
74 | type: nil,
75 | key: key,
76 | elements,
77 | showTypeInfoOnly: true,
78 | isExpanded: elements.count <= itemLimitForExpansion
79 | )
80 |
81 | case let .keyed(key, element: .typed(type, .nested(elements))):
82 | NestedContent(
83 | type: shorthandName(of: type),
84 | key: key,
85 | elements,
86 | showTypeInfoOnly: true,
87 | isExpanded: elements.count <= itemLimitForExpansion
88 | )
89 |
90 | case let .keyed(key, element: .typed(type, .list(elements))):
91 | ListContent(
92 | type: shorthandName(of: type),
93 | key: key,
94 | elements,
95 | showTypeInfoOnly: true,
96 | isExpanded: elements.count <= itemLimitForExpansion
97 | )
98 |
99 | case let .keyed(key, element: .typed(type, .dict(elements))):
100 | DictContent(
101 | type: shorthandName(of: type),
102 | key: key,
103 | elements,
104 | showTypeInfoOnly: true,
105 | isExpanded: elements.count <= itemLimitForExpansion
106 | )
107 |
108 | case let .typed(type, element: .nested(elements)):
109 | NestedContent(
110 | type: shorthandName(of: type),
111 | key: nil,
112 | elements,
113 | showTypeInfoOnly: true,
114 | isExpanded: elements.count <= itemLimitForExpansion
115 | )
116 |
117 | case let .typed(type, element: .list(elements)):
118 | ListContent(
119 | type: shorthandName(of: type),
120 | key: nil,
121 | elements,
122 | showTypeInfoOnly: true,
123 | isExpanded: elements.count <= itemLimitForExpansion
124 | )
125 |
126 | case let .typed(type, element: .dict(elements)):
127 | DictContent(
128 | type: shorthandName(of: type),
129 | key: nil,
130 | elements,
131 | showTypeInfoOnly: true,
132 | isExpanded: elements.count <= itemLimitForExpansion
133 | )
134 |
135 | case let .typed(type, element):
136 | TypedContent(shorthandName(of: type)) {
137 | TypeInfoContentView(element)
138 | }
139 |
140 | case let .keyed(key, element):
141 | KeyedContent(key) {
142 | TypeInfoContentView(element)
143 | }
144 | default:
145 | EmptyView()
146 | }
147 | }
148 | }
149 |
150 |
--------------------------------------------------------------------------------
/Sources/ReflectionView/TypeInfoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TypeInfoView.swift
3 | //
4 | //
5 | // Created by p-x9 on 2024/03/30.
6 | //
7 | //
8 |
9 | import SwiftUI
10 | import SwiftUIColor
11 | import Reflection
12 |
13 | public struct TypeInfoView: View {
14 | let reflection: Reflection
15 |
16 | @Environment(\.font) var font
17 | @Environment(\.reflectionViewConfig) var config
18 |
19 | public init(_ value: Any) {
20 | self.reflection = .init(value)
21 | }
22 |
23 | public var body: some View {
24 | let structured = reflection.structured
25 |
26 | HStack {
27 | TypeInfoContentView(structured)
28 | }
29 | .padding()
30 | }
31 | }
32 |
33 | #Preview {
34 | let value = Text("hello")
35 | return ScrollView {
36 | TypeInfoView(value)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/ReflectionViewTests/ReflectionViewTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Reflection
3 | @testable import ReflectionView
4 |
5 | import SwiftUI
6 |
7 | final class ReflectionViewTests: XCTestCase {
8 | func testParse() {
9 | let text = Text("hello")
10 | let reflection = Reflection(text)
11 | print(reflection.structured)
12 | }
13 |
14 | func testReplacedToOptionalSyntaxSugar_Single() {
15 | XCTAssertEqual("Optional".replacedToOptionalSyntaxSugar, "Int?")
16 | XCTAssertEqual("Optional>".replacedToOptionalSyntaxSugar, "Int??")
17 | XCTAssertEqual("Optional>".replacedToOptionalSyntaxSugar, "Array?")
18 | }
19 |
20 | func testReplacedToArraySyntaxSugar_Single() {
21 | XCTAssertEqual("Array".replacedToArraySyntaxSugar, "[Int]")
22 | XCTAssertEqual("Array>".replacedToArraySyntaxSugar, "[[Int]]")
23 | XCTAssertEqual("Array>".replacedToArraySyntaxSugar, "[Optional]")
24 | }
25 |
26 | func testReplacedToDictionarySyntaxSugar_Single() {
27 | XCTAssertEqual("Dictionary".replacedToDictionarySyntaxSugar, "[String: Int]")
28 | XCTAssertEqual("Dictionary>".replacedToDictionarySyntaxSugar, "[String: [String: Int]]")
29 | XCTAssertEqual("Dictionary>".replacedToDictionarySyntaxSugar, "[String: Optional]")
30 | }
31 |
32 | func testReplacedToCommonSyntaxSugar_Combined() {
33 | let case1 = "Optional>>"
34 | XCTAssertEqual(case1.replacedToCommonSyntaxSugar, "[[String: Int]]?")
35 |
36 | let case2 = "Array>>>"
37 | XCTAssertEqual(case2.replacedToCommonSyntaxSugar, "[[String: Int?]?]")
38 |
39 | let case3 = "Optional, Optional>>>>"
40 | XCTAssertEqual(case3.replacedToCommonSyntaxSugar, "[String?: [Int?]?]?")
41 |
42 | let case4 = "Optional>>>>"
43 | XCTAssertEqual(case4.replacedToCommonSyntaxSugar, "[[String: [String: Int?]]]?")
44 | }
45 | }
46 |
--------------------------------------------------------------------------------