├── .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 | [![Github issues](https://img.shields.io/github/issues/p-x9/swiftui-reflection-view)](https://github.com/p-x9/swiftui-reflection-view/issues) 8 | [![Github forks](https://img.shields.io/github/forks/p-x9/swiftui-reflection-view)](https://github.com/p-x9/swiftui-reflection-view/network/members) 9 | [![Github stars](https://img.shields.io/github/stars/p-x9/swiftui-reflection-view)](https://github.com/p-x9/swiftui-reflection-view/stargazers) 10 | [![Github top language](https://img.shields.io/github/languages/top/p-x9/swiftui-reflection-view)](https://github.com/p-x9/swiftui-reflection-view/) 11 | 12 | ## Demo 13 | 14 | | A | B | C | 15 | | ---- | ---- | ---- | 16 | | A | B | C | 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 | --------------------------------------------------------------------------------