Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f47727df52 | |||
| 2cab78dedc | |||
| 64c536b03c | |||
| ed46533740 | |||
| 0e2595e2d4 | |||
| d7b7c112cf | |||
|
|
3b2f8f926c | ||
|
|
ce9c5bf7d1 | ||
| 18f51ffb93 | |||
| 9f50e272f3 | |||
| 681a51110d | |||
| ee92f63f86 | |||
| a8ce63a67f | |||
| 99603193b9 | |||
| b12c02d391 |
@ -25,7 +25,7 @@ jobs:
|
|||||||
echo "<script>window.location.href += \"/documentation/meta\"</script><p>Please enable JavaScript to view the documentation <a href='/documentation/meta'>here</a>.</p>" > docs/index.html;
|
echo "<script>window.location.href += \"/documentation/meta\"</script><p>Please enable JavaScript to view the documentation <a href='/documentation/meta'>here</a>.</p>" > docs/index.html;
|
||||||
sed -i '' 's/,2px/,10px/g' docs/css/index.*.css
|
sed -i '' 's/,2px/,10px/g' docs/css/index.*.css
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: wangyucode/sftp-upload-action@v2.0.2
|
uses: wangyucode/sftp-upload-action@v2.0.4
|
||||||
with:
|
with:
|
||||||
host: 'volans.uberspace.de'
|
host: 'volans.uberspace.de'
|
||||||
username: 'akforum'
|
username: 'akforum'
|
||||||
|
|||||||
14
CMakeLists.txt
Normal file
14
CMakeLists.txt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.29)
|
||||||
|
project(Meta LANGUAGES Swift)
|
||||||
|
|
||||||
|
if(POLICY CMP0157)
|
||||||
|
cmake_policy(SET CMP0157 NEW)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift)
|
||||||
|
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||||
|
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||||
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
|
|
||||||
|
add_subdirectory(Sources)
|
||||||
|
add_subdirectory(Tests)
|
||||||
@ -24,17 +24,26 @@ let package = Package(
|
|||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "Meta",
|
name: "Meta",
|
||||||
path: "Sources"
|
path: "Sources",
|
||||||
|
exclude: [
|
||||||
|
"CMakeLists.txt"
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SampleBackends",
|
name: "SampleBackends",
|
||||||
dependencies: ["Meta"],
|
dependencies: ["Meta"],
|
||||||
path: "Tests/SampleBackends"
|
path: "Tests/SampleBackends",
|
||||||
|
exclude: [
|
||||||
|
"CMakeLists.txt"
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "DemoApp",
|
name: "DemoApp",
|
||||||
dependencies: ["SampleBackends"],
|
dependencies: ["SampleBackends"],
|
||||||
path: "Tests/DemoApp"
|
path: "Tests/DemoApp",
|
||||||
|
exclude: [
|
||||||
|
"CMakeLists.txt"
|
||||||
|
]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
swiftLanguageModes: [.v5]
|
swiftLanguageModes: [.v5]
|
||||||
|
|||||||
26
Sources/CMakeLists.txt
Normal file
26
Sources/CMakeLists.txt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
file(GLOB META_SOURCES
|
||||||
|
"Model/Data Flow/*.swift"
|
||||||
|
|
||||||
|
"Model/Extensions/*.swift"
|
||||||
|
|
||||||
|
"Model/User Interface/App/*.swift"
|
||||||
|
"Model/User Interface/Scene/*.swift"
|
||||||
|
"Model/User Interface/View/Properties/*.swift"
|
||||||
|
"Model/User Interface/View/*.swift"
|
||||||
|
|
||||||
|
"View/*.swift"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(Meta ${META_SOURCES})
|
||||||
|
|
||||||
|
target_compile_options(Meta PUBLIC -enable-testing)
|
||||||
|
|
||||||
|
set_target_properties(Meta PROPERTIES
|
||||||
|
Swift_LANGUAGE_VERSION 5
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS Meta
|
||||||
|
LIBRARY DESTINATION lib
|
||||||
|
ARCHIVE DESTINATION lib
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
)
|
||||||
@ -373,9 +373,7 @@ For the ``EitherView``, it is again the initializer that has to match a requirem
|
|||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) -> ViewStorage where Data: ViewRenderData {
|
||||||
let view = TermKit.View()
|
let view = TermKit.View()
|
||||||
let storage = ViewStorage(view)
|
return .init(view)
|
||||||
update(storage, data: data, updateProperties: true, type: type)
|
|
||||||
return storage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
@ -386,6 +384,7 @@ Normally, you would initialize all your content storages in the container functi
|
|||||||
In this case, this does not work. If the either view is called via standard `if`/`else` syntax and the condition is `true`,
|
In this case, this does not work. If the either view is called via standard `if`/`else` syntax and the condition is `true`,
|
||||||
we can access `view1`, but `view2` is empty (the actual view is not known). If `condition` is `false`, `view1` is empty and `view2` is known.
|
we can access `view1`, but `view2` is empty (the actual view is not known). If `condition` is `false`, `view1` is empty and `view2` is known.
|
||||||
Therefore, we have to wait with the initialization process until `condition` changes, which is why this is handled in the `update` function.
|
Therefore, we have to wait with the initialization process until `condition` changes, which is why this is handled in the `update` function.
|
||||||
|
Make sure to call the child view's `update` function after constructing a view in the parent view's `update` function.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// ...
|
// ...
|
||||||
@ -406,6 +405,7 @@ Therefore, we have to wait with the initialization process until `condition` cha
|
|||||||
view = content.pointer as? TermKit.View
|
view = content.pointer as? TermKit.View
|
||||||
} else {
|
} else {
|
||||||
let content = body.storage(data: data, type: type)
|
let content = body.storage(data: data, type: type)
|
||||||
|
body.update(content, data: data, updateProperties: true, type: type)
|
||||||
storage.content[condition.description] = [content]
|
storage.content[condition.description] = [content]
|
||||||
view = content.pointer as? TermKit.View
|
view = content.pointer as? TermKit.View
|
||||||
}
|
}
|
||||||
|
|||||||
49
Sources/Model/Data Flow/Environment.swift
Normal file
49
Sources/Model/Data Flow/Environment.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// Environment.swift
|
||||||
|
// Meta
|
||||||
|
//
|
||||||
|
// Created by david-swift on 23.10.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// A property wrapper for properties in a view that should be stored throughout view updates.
|
||||||
|
@propertyWrapper
|
||||||
|
public struct Environment<Value>: EnvironmentProtocol {
|
||||||
|
|
||||||
|
/// Access the environment value.
|
||||||
|
public var wrappedValue: Value? {
|
||||||
|
content.value as? Value
|
||||||
|
}
|
||||||
|
/// The value's identifier.
|
||||||
|
var id: String
|
||||||
|
/// The content.
|
||||||
|
let content = EnvironmentContent()
|
||||||
|
|
||||||
|
// swiftlint:disable function_default_parameter_at_end
|
||||||
|
/// Initialize a property representing an environment value in the view.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - wrappedValue: The wrapped value.
|
||||||
|
/// - id: The environment value's identifier.
|
||||||
|
public init(wrappedValue: Value? = nil, _ id: String) {
|
||||||
|
self.id = id
|
||||||
|
}
|
||||||
|
// swiftlint:enable function_default_parameter_at_end
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An environment property's content.
|
||||||
|
class EnvironmentContent {
|
||||||
|
|
||||||
|
/// The value.
|
||||||
|
var value: Any?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The environment property protocol.
|
||||||
|
protocol EnvironmentProtocol {
|
||||||
|
|
||||||
|
/// The content.
|
||||||
|
var content: EnvironmentContent { get }
|
||||||
|
/// The identifier.
|
||||||
|
var id: String { get }
|
||||||
|
|
||||||
|
}
|
||||||
@ -18,8 +18,10 @@ public struct State<Value>: StateProtocol {
|
|||||||
}
|
}
|
||||||
nonmutating set {
|
nonmutating set {
|
||||||
rawValue = newValue
|
rawValue = newValue
|
||||||
content.update = true
|
if !blockUpdates {
|
||||||
StateManager.updateViews(force: forceUpdates)
|
content.update = true
|
||||||
|
StateManager.updateViews(force: forceUpdates)
|
||||||
|
}
|
||||||
writeValue?(newValue)
|
writeValue?(newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,7 +49,10 @@ public struct State<Value>: StateProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to force update the views when the value changes.
|
/// Whether to force update the views when the value changes.
|
||||||
var forceUpdates: Bool
|
var forceUpdates = false
|
||||||
|
|
||||||
|
/// Whether to block updates.
|
||||||
|
var blockUpdates = false
|
||||||
|
|
||||||
/// The closure for initializing the state property's value.
|
/// The closure for initializing the state property's value.
|
||||||
var getInitialValue: () -> Value
|
var getInitialValue: () -> Value
|
||||||
@ -67,21 +72,35 @@ public struct State<Value>: StateProtocol {
|
|||||||
self.forceUpdates = forceUpdates
|
self.forceUpdates = forceUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize a property representing a state in the view with an autoclosure.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - wrappedValue: The wrapped value.
|
||||||
|
/// - blockUpdates: Whether updates to this state value should not update the UI.
|
||||||
|
///
|
||||||
|
/// This can be useful for storing data and reading this data on special occasions, e.g. on startup.
|
||||||
|
public init(wrappedValue: @autoclosure @escaping () -> Value, blockUpdates: Bool) {
|
||||||
|
getInitialValue = wrappedValue
|
||||||
|
self.blockUpdates = blockUpdates
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize a property representing a state in the view with an explicit closure.
|
/// Initialize a property representing a state in the view with an explicit closure.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - wrappedValue: Get the wrapped value.
|
/// - wrappedValue: Get the wrapped value.
|
||||||
/// - writeValue: Perform additional operations when the value changes.
|
/// - writeValue: Perform additional operations when the value changes.
|
||||||
/// - forceUpdates: Whether to force update all available views when the property gets modified.
|
/// - forceUpdates: Whether to force update all available views when the property gets modified.
|
||||||
|
/// - blockUpdates: Whether updates to this state value should not update the UI.
|
||||||
///
|
///
|
||||||
/// This initializer can be used to get data from the disk.
|
/// This initializer can be used e.g. to get data from the disk.
|
||||||
public init(
|
public init(
|
||||||
wrappedValue: @escaping () -> Value,
|
wrappedValue: @escaping () -> Value,
|
||||||
writeValue: ((Value) -> Void)? = nil,
|
writeValue: ((Value) -> Void)? = nil,
|
||||||
forceUpdates: Bool = false
|
forceUpdates: Bool = false,
|
||||||
|
blockUpdates: Bool = false
|
||||||
) {
|
) {
|
||||||
getInitialValue = wrappedValue
|
getInitialValue = wrappedValue
|
||||||
self.writeValue = writeValue
|
self.writeValue = writeValue
|
||||||
self.forceUpdates = forceUpdates
|
self.forceUpdates = forceUpdates
|
||||||
|
self.blockUpdates = blockUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the initial value.
|
/// Get the initial value.
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
//
|
|
||||||
// OpaquePointer.swift
|
|
||||||
// Meta
|
|
||||||
//
|
|
||||||
// Created by david-swift on 29.09.24.
|
|
||||||
//
|
|
||||||
|
|
||||||
extension OpaquePointer: @retroactive @unchecked Sendable { }
|
|
||||||
@ -45,6 +45,7 @@ extension App {
|
|||||||
for element in app.scene where element is Storage.SceneElementType {
|
for element in app.scene where element is Storage.SceneElementType {
|
||||||
element.setupInitialContainers(app: app.app)
|
element.setupInitialContainers(app: app.app)
|
||||||
}
|
}
|
||||||
|
StateManager.updateViews(force: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,12 @@ extension AppStorage {
|
|||||||
/// Focus the scene element with a certain id (if supported). Create the element if it doesn't already exist.
|
/// Focus the scene element with a certain id (if supported). Create the element if it doesn't already exist.
|
||||||
/// - Parameter id: The element's id.
|
/// - Parameter id: The element's id.
|
||||||
public func showSceneElement(_ id: String) {
|
public func showSceneElement(_ id: String) {
|
||||||
storage.sceneStorage.last { $0.id == id && !$0.destroy }?.show() ?? addSceneElement(id)
|
guard let storage = storage.sceneStorage.last(where: { $0.id == id && !$0.destroy }) else {
|
||||||
|
addSceneElement(id)
|
||||||
|
StateManager.updateViews(force: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storage.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new scene element with the content of the scene element with a certain id.
|
/// Add a new scene element with the content of the scene element with a certain id.
|
||||||
@ -39,6 +44,7 @@ extension AppStorage {
|
|||||||
let container = element.container(app: self)
|
let container = element.container(app: self)
|
||||||
storage.sceneStorage.append(container)
|
storage.sceneStorage.append(container)
|
||||||
showSceneElement(id)
|
showSceneElement(id)
|
||||||
|
StateManager.updateViews(force: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// A view building conditional bodies.
|
/// A view building conditional bodies.
|
||||||
|
///
|
||||||
|
/// Do not forget to call the update function after constructing a new UI.
|
||||||
public protocol EitherView: AnyView {
|
public protocol EitherView: AnyView {
|
||||||
|
|
||||||
/// Initialize the either view.
|
/// Initialize the either view.
|
||||||
|
|||||||
@ -143,7 +143,6 @@ extension Widget {
|
|||||||
) -> ViewStorage where Data: ViewRenderData {
|
) -> ViewStorage where Data: ViewRenderData {
|
||||||
let storage = ViewStorage(initializeWidget())
|
let storage = ViewStorage(initializeWidget())
|
||||||
initProperties(storage, data: data, type: type)
|
initProperties(storage, data: data, type: type)
|
||||||
update(storage, data: data, updateProperties: true, type: type)
|
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ extension View {
|
|||||||
|
|
||||||
/// The view's content.
|
/// The view's content.
|
||||||
public var viewContent: Body {
|
public var viewContent: Body {
|
||||||
[StateWrapper(content: { view }, state: getState())]
|
[StateWrapper(content: { view }, state: getState(), environment: getEnvironmentVariables())]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the state from the properties.
|
/// Get the state from the properties.
|
||||||
@ -47,4 +47,16 @@ extension View {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the environment properties.
|
||||||
|
/// - Returns: The environment properties.
|
||||||
|
func getEnvironmentVariables() -> [String: any EnvironmentProtocol] {
|
||||||
|
var environment: [String: any EnvironmentProtocol] = [:]
|
||||||
|
for property in Mirror(reflecting: self).children {
|
||||||
|
if let label = property.label, let value = property.value as? any EnvironmentProtocol {
|
||||||
|
environment[label] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return environment
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
struct AppearObserver: ConvenienceWidget {
|
struct AppearObserver: ConvenienceWidget {
|
||||||
|
|
||||||
/// The custom code to edit the widget.
|
/// The custom code to edit the widget.
|
||||||
var modify: (ViewStorage) -> Void
|
var modify: (ViewStorage, WidgetData) -> Void
|
||||||
/// The wrapped view.
|
/// The wrapped view.
|
||||||
var content: AnyView
|
var content: AnyView
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ struct AppearObserver: ConvenienceWidget {
|
|||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) -> ViewStorage where Data: ViewRenderData {
|
||||||
let storage = content.storage(data: data, type: type)
|
let storage = content.storage(data: data, type: type)
|
||||||
modify(storage)
|
modify(storage, data)
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,10 +50,17 @@ extension AnyView {
|
|||||||
/// Run a function on the widget when it appears for the first time.
|
/// Run a function on the widget when it appears for the first time.
|
||||||
/// - Parameter closure: The function.
|
/// - Parameter closure: The function.
|
||||||
/// - Returns: A view.
|
/// - Returns: A view.
|
||||||
public func inspectOnAppear(_ closure: @escaping (ViewStorage) -> Void) -> AnyView {
|
public func inspectOnAppear(_ closure: @escaping (ViewStorage, WidgetData) -> Void) -> AnyView {
|
||||||
AppearObserver(modify: closure, content: self)
|
AppearObserver(modify: closure, content: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a function on the widget when it appears for the first time.
|
||||||
|
/// - Parameter closure: The function.
|
||||||
|
/// - Returns: A view.
|
||||||
|
public func inspectOnAppear(_ closure: @escaping (ViewStorage) -> Void) -> AnyView {
|
||||||
|
inspectOnAppear { storage, _ in closure(storage) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Run a function when the view appears for the first time.
|
/// Run a function when the view appears for the first time.
|
||||||
/// - Parameter closure: The function.
|
/// - Parameter closure: The function.
|
||||||
/// - Returns: A view.
|
/// - Returns: A view.
|
||||||
|
|||||||
67
Sources/View/DataWrapper.swift
Normal file
67
Sources/View/DataWrapper.swift
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
//
|
||||||
|
// StateWrapper.swift
|
||||||
|
// Meta
|
||||||
|
//
|
||||||
|
// Created by david-swift on 09.06.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Assign values to the environment.
|
||||||
|
///
|
||||||
|
/// Access the environment in views (``View``) via `@Environment`.
|
||||||
|
struct DataWrapper: ConvenienceWidget {
|
||||||
|
|
||||||
|
/// The content.
|
||||||
|
var content: Body
|
||||||
|
/// The identifier for the new environment value.
|
||||||
|
var label: String
|
||||||
|
/// The environment value.
|
||||||
|
var data: Any
|
||||||
|
|
||||||
|
/// Get a view storage.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - data: Modify views before being updated.
|
||||||
|
/// - type: The view render data type.
|
||||||
|
/// - Returns: The view storage.
|
||||||
|
func container<Data>(
|
||||||
|
data: WidgetData,
|
||||||
|
type: Data.Type
|
||||||
|
) -> ViewStorage where Data: ViewRenderData {
|
||||||
|
content.storage(data: data.modify { $0.fields[label] = self.data }, type: type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a view storage.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - storage: The view storage.
|
||||||
|
/// - data: Modify views before being updated.
|
||||||
|
/// - updateProperties: Whether to update properties.
|
||||||
|
/// - type: The view render data type.
|
||||||
|
/// - Returns: The view storage.
|
||||||
|
func update<Data>(
|
||||||
|
_ storage: ViewStorage,
|
||||||
|
data: WidgetData,
|
||||||
|
updateProperties: Bool,
|
||||||
|
type: Data.Type
|
||||||
|
) where Data: ViewRenderData {
|
||||||
|
content
|
||||||
|
.updateStorage(
|
||||||
|
storage,
|
||||||
|
data: data.modify { $0.fields[label] = self.data },
|
||||||
|
updateProperties: updateProperties,
|
||||||
|
type: type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnyView {
|
||||||
|
|
||||||
|
/// Assign a value to an environment label.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - label: The environment label.
|
||||||
|
/// - data: The value.
|
||||||
|
/// - Returns: The view.
|
||||||
|
public func environment(_ label: String, data: Any) -> AnyView {
|
||||||
|
DataWrapper(content: [self], label: label, data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@
|
|||||||
struct InspectorWrapper: ConvenienceWidget {
|
struct InspectorWrapper: ConvenienceWidget {
|
||||||
|
|
||||||
/// The custom code to edit the widget.
|
/// The custom code to edit the widget.
|
||||||
var modify: (ViewStorage, Bool) -> Void
|
var modify: (ViewStorage, WidgetData, Bool) -> Void
|
||||||
/// The wrapped view.
|
/// The wrapped view.
|
||||||
var content: AnyView
|
var content: AnyView
|
||||||
|
|
||||||
@ -22,9 +22,7 @@ struct InspectorWrapper: ConvenienceWidget {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) -> ViewStorage where Data: ViewRenderData {
|
||||||
let storage = content.storage(data: data, type: type)
|
content.storage(data: data, type: type)
|
||||||
modify(storage, true)
|
|
||||||
return storage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the stored content.
|
/// Update the stored content.
|
||||||
@ -40,7 +38,7 @@ struct InspectorWrapper: ConvenienceWidget {
|
|||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) where Data: ViewRenderData {
|
||||||
content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
modify(storage, updateProperties)
|
modify(storage, data, updateProperties)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -51,10 +49,17 @@ extension AnyView {
|
|||||||
/// Run a custom code accessing the view's storage when initializing and updating the view.
|
/// Run a custom code accessing the view's storage when initializing and updating the view.
|
||||||
/// - Parameter modify: Modify the storage. The boolean indicates whether state in parent views changed.
|
/// - Parameter modify: Modify the storage. The boolean indicates whether state in parent views changed.
|
||||||
/// - Returns: A view.
|
/// - Returns: A view.
|
||||||
public func inspect(_ modify: @escaping (ViewStorage, Bool) -> Void) -> AnyView {
|
public func inspect(_ modify: @escaping (ViewStorage, WidgetData, Bool) -> Void) -> AnyView {
|
||||||
InspectorWrapper(modify: modify, content: self)
|
InspectorWrapper(modify: modify, content: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a custom code accessing the view's storage when initializing and updating the view.
|
||||||
|
/// - Parameter modify: Modify the storage. The boolean indicates whether state in parent views changed.
|
||||||
|
/// - Returns: A view.
|
||||||
|
public func inspect(_ modify: @escaping (ViewStorage, Bool) -> Void) -> AnyView {
|
||||||
|
inspect { storage, _, updateProperties in modify(storage, updateProperties) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Run a function when the view gets updated.
|
/// Run a function when the view gets updated.
|
||||||
/// - Parameter onUpdate: The function.
|
/// - Parameter onUpdate: The function.
|
||||||
/// - Returns: A view.
|
/// - Returns: A view.
|
||||||
|
|||||||
91
Sources/View/SafeWrapper.swift
Normal file
91
Sources/View/SafeWrapper.swift
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// SafeWrapper.swift
|
||||||
|
// Meta
|
||||||
|
//
|
||||||
|
// Created by david-swift on 02.02.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Wrap a widget but keep its pointer.
|
||||||
|
struct SafeWrapper: ConvenienceWidget {
|
||||||
|
|
||||||
|
/// The custom code to edit the wrapper.
|
||||||
|
/// The pointer is the one of the child widget.
|
||||||
|
var modify: (ViewStorage, WidgetData, Bool) -> Void
|
||||||
|
/// The wrapped view.
|
||||||
|
var content: AnyView
|
||||||
|
|
||||||
|
/// The view storage.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - data: Modify views before being updated.
|
||||||
|
/// - type: The view render data type.
|
||||||
|
/// - Returns: The view storage.
|
||||||
|
func container<Data>(
|
||||||
|
data: WidgetData,
|
||||||
|
type: Data.Type
|
||||||
|
) -> ViewStorage where Data: ViewRenderData {
|
||||||
|
let contentStorage = content.storage(data: data, type: type)
|
||||||
|
return .init(contentStorage.pointer, content: [.mainContent: [contentStorage]])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 update<Data>(
|
||||||
|
_ storage: ViewStorage,
|
||||||
|
data: WidgetData,
|
||||||
|
updateProperties: Bool,
|
||||||
|
type: Data.Type
|
||||||
|
) where Data: ViewRenderData {
|
||||||
|
if let contentStorage = storage.content[.mainContent]?.first {
|
||||||
|
content.updateStorage(contentStorage, data: data, updateProperties: updateProperties, type: type)
|
||||||
|
}
|
||||||
|
modify(storage, data, updateProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extend any view.
|
||||||
|
extension AnyView {
|
||||||
|
|
||||||
|
/// Wrap a widget but keep its pointer.
|
||||||
|
/// - Parameter modify: Modify the storage. The boolean indicates whether state in parent views changed.
|
||||||
|
/// - Returns: A view.
|
||||||
|
public func wrap(_ modify: @escaping (ViewStorage, WidgetData, Bool) -> Void) -> AnyView {
|
||||||
|
SafeWrapper(modify: modify, content: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrap a widget but keep its pointer.
|
||||||
|
/// - Parameter modify: Modify the storage. The boolean indicates whether state in parent views changed.
|
||||||
|
/// - Returns: A view.
|
||||||
|
public func wrap(_ modify: @escaping (ViewStorage, Bool) -> Void) -> AnyView {
|
||||||
|
wrap { storage, _, updateProperties in modify(storage, updateProperties) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper for generic simple modifiers.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - properties: The properties will be stored. Do not change the layout throughout updates.
|
||||||
|
/// - update: If properties change, run this function.
|
||||||
|
/// - Returns: A view.
|
||||||
|
public func wrapModifier(properties: [any Hashable], update: @escaping (ViewStorage) -> Void) -> AnyView {
|
||||||
|
wrap { storage, _, updateProperties in
|
||||||
|
guard updateProperties else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var shouldUpdate = false
|
||||||
|
for (index, property) in properties.enumerated() {
|
||||||
|
let update = {
|
||||||
|
shouldUpdate = true
|
||||||
|
storage.fields[index.description] = property
|
||||||
|
}
|
||||||
|
if let equatable = storage.fields[index.description] as? any Hashable {
|
||||||
|
if property.hashValue != equatable.hashValue { update() }
|
||||||
|
} else { update() }
|
||||||
|
}
|
||||||
|
if shouldUpdate { update(storage) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -12,6 +12,8 @@ struct StateWrapper: ConvenienceWidget {
|
|||||||
var content: () -> Body
|
var content: () -> Body
|
||||||
/// The state information (from properties with the `State` wrapper).
|
/// The state information (from properties with the `State` wrapper).
|
||||||
var state: [String: StateProtocol] = [:]
|
var state: [String: StateProtocol] = [:]
|
||||||
|
/// The environment properties.
|
||||||
|
var environment: [String: any EnvironmentProtocol] = [:]
|
||||||
|
|
||||||
/// Initialize a `StateWrapper`.
|
/// Initialize a `StateWrapper`.
|
||||||
/// - Parameter content: The view content.
|
/// - Parameter content: The view content.
|
||||||
@ -23,9 +25,15 @@ struct StateWrapper: ConvenienceWidget {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - content: The view content.
|
/// - content: The view content.
|
||||||
/// - state: The state information.
|
/// - state: The state information.
|
||||||
init(content: @escaping () -> Body, state: [String: StateProtocol]) {
|
/// - environment: The environment properties.
|
||||||
|
init(
|
||||||
|
content: @escaping () -> Body,
|
||||||
|
state: [String: StateProtocol],
|
||||||
|
environment: [String: any EnvironmentProtocol]
|
||||||
|
) {
|
||||||
self.content = content
|
self.content = content
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.environment = environment
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a view storage.
|
/// Update a view storage.
|
||||||
@ -51,6 +59,7 @@ struct StateWrapper: ConvenienceWidget {
|
|||||||
property.value.content.update = false
|
property.value.content.update = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
assignEnvironment(data: data)
|
||||||
guard let storage = storage.content[.mainContent]?.first else {
|
guard let storage = storage.content[.mainContent]?.first else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -66,6 +75,7 @@ struct StateWrapper: ConvenienceWidget {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) -> ViewStorage where Data: ViewRenderData {
|
||||||
|
assignEnvironment(data: data)
|
||||||
let content = content().storage(data: data, type: type)
|
let content = content().storage(data: data, type: type)
|
||||||
let storage = ViewStorage(content.pointer, content: [.mainContent: [content]])
|
let storage = ViewStorage(content.pointer, content: [.mainContent: [content]])
|
||||||
storage.state = state
|
storage.state = state
|
||||||
@ -75,4 +85,12 @@ struct StateWrapper: ConvenienceWidget {
|
|||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assign an environment value to the environment property.
|
||||||
|
/// - Parameter data: The widget data.
|
||||||
|
func assignEnvironment(data: WidgetData) {
|
||||||
|
for property in environment {
|
||||||
|
property.value.content.value = data.fields[property.value.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
Tests/CMakeLists.txt
Normal file
2
Tests/CMakeLists.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
add_subdirectory(SampleBackends)
|
||||||
|
add_subdirectory(DemoApp)
|
||||||
13
Tests/DemoApp/CMakeLists.txt
Normal file
13
Tests/DemoApp/CMakeLists.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
add_executable(DemoApp
|
||||||
|
DemoApp.swift
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_options(DemoApp PUBLIC
|
||||||
|
-parse-as-library
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(DemoApp PRIVATE SampleBackends)
|
||||||
|
|
||||||
|
set_target_properties(DemoApp PROPERTIES
|
||||||
|
Swift_LANGUAGE_VERSION 5
|
||||||
|
)
|
||||||
@ -23,6 +23,7 @@ struct DemoApp: App {
|
|||||||
var scene: Scene {
|
var scene: Scene {
|
||||||
Backend1.Window("main", spawn: 1) {
|
Backend1.Window("main", spawn: 1) {
|
||||||
DemoView(app: app)
|
DemoView(app: app)
|
||||||
|
.environment("test", data: 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,6 +32,8 @@ struct DemoApp: App {
|
|||||||
struct DemoView: View {
|
struct DemoView: View {
|
||||||
|
|
||||||
@State private var model = TestModel()
|
@State private var model = TestModel()
|
||||||
|
@Environment("test")
|
||||||
|
private var test: Int?
|
||||||
var app: any AppStorage
|
var app: any AppStorage
|
||||||
let condition = false
|
let condition = false
|
||||||
|
|
||||||
@ -46,6 +49,7 @@ struct DemoView: View {
|
|||||||
app.addSceneElement("main")
|
app.addSceneElement("main")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear { print(test ?? 0) }
|
||||||
}
|
}
|
||||||
TestView()
|
TestView()
|
||||||
testContent
|
testContent
|
||||||
|
|||||||
12
Tests/SampleBackends/CMakeLists.txt
Normal file
12
Tests/SampleBackends/CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
add_library(SampleBackends
|
||||||
|
Backend1.swift
|
||||||
|
Backend2.swift
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(SampleBackends
|
||||||
|
PRIVATE Meta
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(SampleBackends PROPERTIES
|
||||||
|
Swift_LANGUAGE_VERSION 5
|
||||||
|
)
|
||||||
Loading…
x
Reference in New Issue
Block a user