Hermes Agent 36967370fd SwiftBricks: Bricks JSON UI framework native Swift/SwiftUI implementation
- Core: Schema (Codable), Store, EventBus, RPC, Engine, I18n
- Controls: Text, Title1-6, Label, Input, Textarea, Number, Date, Button, Select
  VBox, HBox, ScrollPanel, DynamicColumn, Tabular, Form, InlineForm,
  TabView, Menu, PopupWindow, Html, Image, UrlWidget
- Renderer: BricksView (main entry), ControlRenderer (recursive dispatch)
- 30+ widget types, 9 bind actiontypes
- Form validation (required/minlength/maxlength/min/max/email/number/pattern)
- i18n with multi-language JSON files
- 18 tests covering schema parsing, store, i18n, events
2026-06-21 12:12:03 +08:00

159 lines
4.8 KiB
Swift

import SwiftUI
/// BricksView SwiftBricks
/// JSON schemaUI
///
/// :
/// ```swift
/// BricksView(json: jsonString, engine: engine)
/// BricksView(url: "/api/page.dspy", engine: engine)
/// ```
public struct BricksView: View {
@ObservedObject var engine: BricksEngine
@State private var isLoaded: Bool = false
@State private var loadError: String?
private var jsonString: String?
private var loadURL: String?
/// JSON
public init(json: String, engine: BricksEngine) {
self.jsonString = json
self.loadURL = nil
self._engine = ObservedObject(wrappedValue: engine)
}
/// URL
public init(url: String, engine: BricksEngine) {
self.jsonString = nil
self.loadURL = url
self._engine = ObservedObject(wrappedValue: engine)
}
/// schema
public init(schema: ControlSchema, engine: BricksEngine) {
self.jsonString = nil
self.loadURL = nil
self._engine = ObservedObject(wrappedValue: engine)
//
engine.loadSchema(schema)
}
public var body: some View {
ZStack {
if let schema = engine.rootSchema {
//
ControlRenderer(schema: schema, engine: engine)
.frame(maxWidth: .infinity, maxHeight: .infinity)
//
if let popup = engine.activePopup {
popupOverlay(popup)
}
//
if engine.isLoading {
Color.black.opacity(0.01)
.overlay(ProgressView())
}
} else if let error = loadError {
errorView(error)
} else {
ProgressView("加载中...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.task {
await performLoad()
}
.alert("错误", isPresented: .init(
get: { engine.errorMessage != nil },
set: { if !$0 { engine.errorMessage = nil } }
)) {
Button("确定") { engine.errorMessage = nil }
} message: {
Text(engine.errorMessage ?? "")
}
}
// MARK: -
private func performLoad() async {
if let json = jsonString {
do {
try engine.loadJSON(json)
isLoaded = true
} catch {
loadError = "JSON解析失败: \(error.localizedDescription)"
}
} else if let url = loadURL {
do {
try await engine.loadFromURL(url)
isLoaded = true
} catch {
loadError = error.localizedDescription
}
}
}
// MARK: -
private func popupOverlay(_ popup: PopupInfo) -> some View {
ZStack {
//
Color.black.opacity(0.3)
.ignoresSafeArea()
.onTapGesture {
engine.activePopup = nil
}
//
PopupWindowControl(schema: popup.schema, engine: engine)
.frame(
maxWidth: popupWidth(popup.options),
maxHeight: popupHeight(popup.options)
)
.padding(20)
}
}
private func popupWidth(_ opts: PopupOptions) -> CGFloat? {
if let cw = opts.cwidth { return CGFloat(cw * 8) }
if let w = opts.width, let px = Double(w.replacingOccurrences(of: "px", with: "")) {
return CGFloat(px)
}
return 400
}
private func popupHeight(_ opts: PopupOptions) -> CGFloat? {
if let ch = opts.cheight { return CGFloat(ch * 16) }
if let h = opts.height {
if h.hasSuffix("%") { return nil } // maxHeight
if let px = Double(h.replacingOccurrences(of: "px", with: "")) {
return CGFloat(px)
}
}
return nil
}
// MARK: -
private func errorView(_ error: String) -> some View {
VStack(spacing: 12) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.largeTitle)
.foregroundColor(.orange)
Text("加载失败")
.font(.headline)
Text(error)
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Button("重试") {
Task { await performLoad() }
}
}
.padding(40)
}
}