// // 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: 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: 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( _ 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( _ 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: 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() { } }