diff --git a/Sources/JSONViewer/FontConfiguration.swift b/Sources/JSONViewer/FontConfiguration.swift new file mode 100644 index 0000000..84153fa --- /dev/null +++ b/Sources/JSONViewer/FontConfiguration.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by krishna varshney on 24/10/23. +// + +import Foundation +import SwiftUI + +public struct JSONViewerFontConfiguration { + let keyFont: Font + let valueFont: Font + + public init(keyFont: Font, valueFont: Font) { + self.keyFont = keyFont + self.valueFont = valueFont + } + + public init(with font: Font) { + self.keyFont = font + self.valueFont = font + } + + public init() { + self.init(with: .system(size: 14)) + } + +} diff --git a/Sources/JSONViewer/JSONNodeView.swift b/Sources/JSONViewer/JSONNodeView.swift index d0b9b8a..99705fa 100644 --- a/Sources/JSONViewer/JSONNodeView.swift +++ b/Sources/JSONViewer/JSONNodeView.swift @@ -8,70 +8,119 @@ import SwiftUI public struct JSONNodeView: View { - var node: JSONNode - var level: Int - @State var expanded: [String: Bool] + let node: JSONNode + let level: Int + @Binding var fontConfiguration: JSONViewerFontConfiguration + @State var expandedNodes: [String: Bool] + + internal init(node: JSONNode, level: Int, expandedNodes: [String: Bool], fontConfiguration: Binding) { + self.node = node + self.level = level + self._expandedNodes = State(initialValue: expandedNodes) + self._fontConfiguration = fontConfiguration + } + public var body: some View { VStack { if node.isExpandable { - VStack(alignment: .trailing) { + expandableNodeView() + } else { + nonExpandableNodeView() + } + } + } + + func nonExpandableNodeView() -> some View { + HStack { + Spacer() + .frame(width: 32 * CGFloat(level)) + Circle() + .fill(.white) + .frame(width: 8, height: 8) + HStack(spacing: 0) { + Text("\(node.key)") + .font(fontConfiguration.keyFont) + Text(":") + Text("\(node.value)") + .font(fontConfiguration.valueFont) + } + Spacer() + } + } + + func nodeToggleButtonIcon() -> some View { + if self.expandedNodes[node.key] ?? false { + Image(systemName: "minus.circle.fill") + .imageScale(.large) + .frame(width: 16, height: 16) + } else { + Image(systemName: "plus.circle.fill") + .imageScale(.large) + .frame(width: 16, height: 16) + } + } + + func expandableNodeTypeLabel() -> some View { + if node.type == .object { + return AnyView( + Image(systemName: "curlybraces") + .font(fontConfiguration.keyFont) + .frame(width: 16, height: 16) + ) + + } else if node.type == .array { + return AnyView( + Text("[ ]") + .font(fontConfiguration.keyFont) + .frame(width: 16, height: 16) + ) + } + return AnyView(EmptyView()) + } + + func toggleNodeState() { + if let value = self.expandedNodes[node.key] { + self.expandedNodes[node.key] = !value + } else { + self.expandedNodes[node.key] = true + } + } + + func nodeSuccessorView() -> some View { + VStack(alignment: .trailing) { + ForEach(node.children) { childNode in + HStack() { + JSONNodeView(node: childNode, + level: level + 1, + expandedNodes: [String : Bool](), + fontConfiguration: self.$fontConfiguration) + } + } + } + } + + func expandableNodeView() -> some View { + VStack(alignment: .trailing) { + HStack { + Spacer() + .frame(width: 32 * CGFloat(level)) + Button { + toggleNodeState() + } label: { HStack { - Spacer() - .frame(width: 32 * CGFloat(level)) - Button { - if let value = self.expanded[node.key] { - self.expanded[node.key] = !value - } else { - self.expanded[node.key] = true - } - } label: { - HStack { - if self.expanded[node.key] ?? false { - Image(systemName: "minus.circle.fill") - .imageScale(.large) - .frame(width: 16, height: 16) - } else { - Image(systemName: "plus.circle.fill") - .imageScale(.large) - .frame(width: 16, height: 16) - } - - if node.type == .object { - Image(systemName: "curlybraces") - .frame(width: 16, height: 16) - } else if node.type == .array { - Text("[ ]") - .frame(width: 16, height: 16) - } - Text(node.key) - } - } - Spacer() - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - .buttonStyle(PlainButtonStyle()) - if self.expanded[node.key] ?? false { - VStack(alignment: .trailing) { - ForEach(node.children) { childNode in - HStack() { - JSONNodeView(node: childNode, level: level + 1, expanded: [String : Bool]()) - } - } - } + nodeToggleButtonIcon() + expandableNodeTypeLabel() + Text(node.key) + .font(fontConfiguration.keyFont) } } - } else { - HStack { - Spacer() - .frame(width: 32 * CGFloat(level)) - Circle() - .fill(.white) - .frame(width: 8, height: 8) - Text("\(node.key): \(node.value)") - .font(.headline) - Spacer() - } + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .buttonStyle(PlainButtonStyle()) + + if self.expandedNodes[node.key] ?? false { + nodeSuccessorView() } } } diff --git a/Sources/JSONViewer/JSONViewer.swift b/Sources/JSONViewer/JSONViewer.swift index 81d8c02..7bf808f 100644 --- a/Sources/JSONViewer/JSONViewer.swift +++ b/Sources/JSONViewer/JSONViewer.swift @@ -1,17 +1,30 @@ import SwiftUI public struct JSONViewer: View { - var rootNode: JSONNode + @Binding var fontConfiguration: JSONViewerFontConfiguration + private let rootNode: JSONNode + private let expandedNodes: [String: Bool] - public init(rootNode: JSONNode) { + public init(rootNode: JSONNode, isRootExpanded: Bool = true) { self.rootNode = rootNode + self.expandedNodes = isRootExpanded ? ["Root": true] : [:] + self._fontConfiguration = Binding.constant(JSONViewerFontConfiguration()) + } + + public init(rootNode: JSONNode, fontConfiguration: Binding, isRootExpanded: Bool = true) { + self.rootNode = rootNode + self.expandedNodes = isRootExpanded ? ["Root": true] : [:] + self._fontConfiguration = fontConfiguration } public var body: some View { HStack { VStack { ScrollView { - JSONNodeView(node: rootNode, level: 0, expanded: ["Root": true]) + JSONNodeView(node: rootNode, + level: 0, + expandedNodes: self.expandedNodes, + fontConfiguration: $fontConfiguration) } .scrollIndicators(.hidden) Spacer()