macbackend/Sources/Core/Model/SwiftUI/SwiftUIWidget.swift
david-swift 319797a68d
All checks were successful
Deploy Docs / publish (push) Successful in 4m3s
SwiftLint / SwiftLint (push) Successful in 4s
Fix publishing changes from within view updates
2025-01-18 20:31:54 +01:00

178 lines
5.4 KiB
Swift

//
// SwiftUIWidget.swift
// MacBackend
//
// Created by david-swift on 25.11.2024.
//
import LevenshteinTransformations
import SwiftUI
/// Wrap a SwiftUI widget to be used inside a `MacBackend` view.
public protocol SwiftUIWidget: MacWidget {
/// The content SwiftUI view.
associatedtype Content: SwiftUI.View
/// The wrapped views.
var wrappedViews: [String: Meta.AnyView] { get }
/// Get the SwiftUI view.
/// - Parameter properties: The widget data.
/// - Returns: The SwiftUI view.
@SwiftUI.ViewBuilder
static func view(properties: Self) -> Content
}
extension SwiftUIWidget {
/// The wrapped views.
public var wrappedViews: [String: Meta.AnyView] {
[:]
}
/// The view storage.
/// - Parameters:
/// - data: Modify views before being updated.
/// - type: The view render data type.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
internalContainer(data: data, type: type)
}
/// The view storage.
/// - Parameters:
/// - data: Modify views before being updated.
/// - type: The view render data type.
/// - Returns: The view storage.
func internalContainer<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let id = UUID().uuidString
let updater = SwiftUIUpdater.updater
updater.state[id] = self
let wrappedStorages = wrappedViews.reduce(into: [String: ViewStorage]()) { partialResult, element in
partialResult[element.key] = element.value.storage(data: data, type: type)
}
let storage: ViewStorage = .init(nil)
storage.fields["child-storages"] = wrappedStorages
let view = NSHostingView(
rootView: SwiftUIWrapperView(updater: updater, id: id, data: data) { value in
if let value = value as? Self {
Self.view(properties: value)
.environment(\.views, storage.fields["child-storages"] as? [String: ViewStorage])
}
}
)
storage.pointer = view
storage.fields["updater-id"] = id
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The view render data type.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
internalUpdate(storage, data: data, updateProperties: updateProperties, type: type)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The view render data type.
func internalUpdate<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
if updateProperties, let id = storage.fields["updater-id"] as? String {
Task { @MainActor in
SwiftUIUpdater.updater.state[id] = self
}
}
var children = storage.fields["child-storages"] as? [String: ViewStorage] ?? [:]
for view in wrappedViews where !children.contains(where: { $0.key == view.key }) {
children[view.key] = view.value.storage(data: data, type: type)
}
for view in children where !wrappedViews.contains(where: { $0.key == view.key }) {
children[view.key] = nil
}
storage.fields["child-storages"] = children
for (key, storage) in children {
wrappedViews[key]?.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
}
}
}
/// A SwiftUI view which can be displayed and updated inside a `MacBackend` widget.
struct SwiftUIWrapperView<Content>: SwiftUI.View where Content: SwiftUI.View {
/// The updater observable object.
@ObservedObject var updater: SwiftUIUpdater
/// The identifier.
var id: String
/// The widget data.
var data: WidgetData
/// The wrapped view.
var view: (Any) -> Content
/// The SwiftUI view content.
var body: some SwiftUI.View {
if let state = updater.state[id] {
view(state)
}
}
/// Initialize the SwiftUI wrapper view.
/// - Parameters:
/// - updater: The updater observable object.
/// - id: The identifier.
/// - data: The widget data.
/// - view: The wrapped view.
init(updater: SwiftUIUpdater, id: String, data: WidgetData, @SwiftUI.ViewBuilder view: @escaping (Any) -> Content) {
self.updater = updater
self.id = id
self.view = view
self.data = data
}
}
extension EnvironmentValues {
/// The views environment value.
@Entry var views: [String: ViewStorage]?
}
/// The SwiftUI updater object.
class SwiftUIUpdater: ObservableObject {
/// The updater.
static var updater: SwiftUIUpdater = .init()
/// The state for SwiftUI views.
@Published var state: [String: Any] = [:]
/// Initialize an updater.
init() { }
}