Compare commits

...

15 Commits
0.1.0 ... main

Author SHA1 Message Date
f47727df52 Add wrap modifier
All checks were successful
SwiftLint / SwiftLint (push) Successful in 7s
Deploy Docs / publish (push) Successful in 3m23s
2026-02-03 00:06:52 +01:00
2cab78dedc Implement new update logic in wrappers
Some checks are pending
Deploy Docs / publish (push) Waiting to run
SwiftLint / SwiftLint (push) Waiting to run
2026-02-02 23:15:04 +01:00
64c536b03c Add EitherView instructions for new update system
Some checks are pending
Deploy Docs / publish (push) Waiting to run
SwiftLint / SwiftLint (push) Waiting to run
2026-02-02 22:29:52 +01:00
ed46533740 App updates automatically after constructing UI
Some checks are pending
Deploy Docs / publish (push) Waiting to run
SwiftLint / SwiftLint (push) Waiting to run
Before, a backend had to implement the update right after the
construction of the UI manually
2026-02-02 22:07:57 +01:00
0e2595e2d4 Remove explicit OpaquePointer Sendable conformance
Some checks are pending
Deploy Docs / publish (push) Waiting to run
SwiftLint / SwiftLint (push) Waiting to run
2026-02-02 20:14:25 +01:00
d7b7c112cf Add SafeWrapper
Some checks are pending
Deploy Docs / publish (push) Waiting to run
SwiftLint / SwiftLint (push) Waiting to run
2026-02-02 20:13:42 +01:00
david-swift
3b2f8f926c Use latest version of SFTP upload action
All checks were successful
Deploy Docs / publish (push) Successful in 3m2s
2025-04-06 16:47:29 +02:00
david-swift
ce9c5bf7d1 Merge pull request 'Add CMake support' (#2) from Zaph/meta:main into main
Some checks failed
Deploy Docs / publish (push) Failing after 25s
SwiftLint / SwiftLint (push) Successful in 3s
Reviewed-on: #2
Reviewed-by: david-swift <david-swift@noreply.aproksha.uber.space>
2025-04-05 18:33:45 +02:00
18f51ffb93 Add CMake support
Some checks failed
SwiftLint / SwiftLint (pull_request) Has been cancelled
2025-04-02 20:22:43 +01:00
9f50e272f3 Add support for accessing the widget data when inspecting
All checks were successful
Deploy Docs / publish (push) Successful in 55s
SwiftLint / SwiftLint (push) Successful in 3s
2025-02-10 17:54:22 +01:00
681a51110d Add support for non-updating state properties
All checks were successful
Deploy Docs / publish (push) Successful in 44s
SwiftLint / SwiftLint (push) Successful in 4s
2024-11-11 12:59:18 +01:00
ee92f63f86 Add support for environment properties
All checks were successful
Deploy Docs / publish (push) Successful in 50s
SwiftLint / SwiftLint (push) Successful in 4s
2024-10-24 13:16:23 +02:00
a8ce63a67f Undo equatable bindings due to complexity
All checks were successful
Deploy Docs / publish (push) Successful in 45s
SwiftLint / SwiftLint (push) Successful in 2s
2024-10-20 14:02:32 +02:00
99603193b9 Fix latest binding change making binding view-only
Some checks failed
Deploy Docs / publish (push) Successful in 45s
SwiftLint / SwiftLint (push) Failing after 2s
2024-10-20 13:57:44 +02:00
b12c02d391 Make binding equatable where value is equatable
Some checks failed
Deploy Docs / publish (push) Successful in 46s
SwiftLint / SwiftLint (push) Failing after 2s
2024-10-20 13:47:16 +02:00
22 changed files with 381 additions and 33 deletions

View File

@ -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;
sed -i '' 's/,2px/,10px/g' docs/css/index.*.css
- name: Upload
uses: wangyucode/sftp-upload-action@v2.0.2
uses: wangyucode/sftp-upload-action@v2.0.4
with:
host: 'volans.uberspace.de'
username: 'akforum'

14
CMakeLists.txt Normal file
View 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)

View File

@ -24,17 +24,26 @@ let package = Package(
targets: [
.target(
name: "Meta",
path: "Sources"
path: "Sources",
exclude: [
"CMakeLists.txt"
]
),
.target(
name: "SampleBackends",
dependencies: ["Meta"],
path: "Tests/SampleBackends"
path: "Tests/SampleBackends",
exclude: [
"CMakeLists.txt"
]
),
.executableTarget(
name: "DemoApp",
dependencies: ["SampleBackends"],
path: "Tests/DemoApp"
path: "Tests/DemoApp",
exclude: [
"CMakeLists.txt"
]
)
],
swiftLanguageModes: [.v5]

26
Sources/CMakeLists.txt Normal file
View 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
)

View File

@ -373,9 +373,7 @@ For the ``EitherView``, it is again the initializer that has to match a requirem
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let view = TermKit.View()
let storage = ViewStorage(view)
update(storage, data: data, updateProperties: true, type: type)
return storage
return .init(view)
}
// ...
@ -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`,
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.
Make sure to call the child view's `update` function after constructing a view in the parent view's `update` function.
```swift
// ...
@ -406,6 +405,7 @@ Therefore, we have to wait with the initialization process until `condition` cha
view = content.pointer as? TermKit.View
} else {
let content = body.storage(data: data, type: type)
body.update(content, data: data, updateProperties: true, type: type)
storage.content[condition.description] = [content]
view = content.pointer as? TermKit.View
}

View 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 }
}

View File

@ -18,8 +18,10 @@ public struct State<Value>: StateProtocol {
}
nonmutating set {
rawValue = newValue
content.update = true
StateManager.updateViews(force: forceUpdates)
if !blockUpdates {
content.update = true
StateManager.updateViews(force: forceUpdates)
}
writeValue?(newValue)
}
}
@ -47,7 +49,10 @@ public struct State<Value>: StateProtocol {
}
/// 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.
var getInitialValue: () -> Value
@ -67,21 +72,35 @@ public struct State<Value>: StateProtocol {
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.
/// - Parameters:
/// - wrappedValue: Get the wrapped value.
/// - writeValue: Perform additional operations when the value changes.
/// - 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(
wrappedValue: @escaping () -> Value,
writeValue: ((Value) -> Void)? = nil,
forceUpdates: Bool = false
forceUpdates: Bool = false,
blockUpdates: Bool = false
) {
getInitialValue = wrappedValue
self.writeValue = writeValue
self.forceUpdates = forceUpdates
self.blockUpdates = blockUpdates
}
/// Get the initial value.

View File

@ -1,8 +0,0 @@
//
// OpaquePointer.swift
// Meta
//
// Created by david-swift on 29.09.24.
//
extension OpaquePointer: @retroactive @unchecked Sendable { }

View File

@ -45,6 +45,7 @@ extension App {
for element in app.scene where element is Storage.SceneElementType {
element.setupInitialContainers(app: app.app)
}
StateManager.updateViews(force: true)
}
}

View File

@ -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.
/// - Parameter id: The element's id.
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.
@ -39,6 +44,7 @@ extension AppStorage {
let container = element.container(app: self)
storage.sceneStorage.append(container)
showSceneElement(id)
StateManager.updateViews(force: true)
}
}

View File

@ -6,6 +6,8 @@
//
/// A view building conditional bodies.
///
/// Do not forget to call the update function after constructing a new UI.
public protocol EitherView: AnyView {
/// Initialize the either view.

View File

@ -143,7 +143,6 @@ extension Widget {
) -> ViewStorage where Data: ViewRenderData {
let storage = ViewStorage(initializeWidget())
initProperties(storage, data: data, type: type)
update(storage, data: data, updateProperties: true, type: type)
return storage
}

View File

@ -32,7 +32,7 @@ extension View {
/// The view's content.
public var viewContent: Body {
[StateWrapper(content: { view }, state: getState())]
[StateWrapper(content: { view }, state: getState(), environment: getEnvironmentVariables())]
}
/// Get the state from the properties.
@ -47,4 +47,16 @@ extension View {
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
}
}

View File

@ -9,7 +9,7 @@
struct AppearObserver: ConvenienceWidget {
/// The custom code to edit the widget.
var modify: (ViewStorage) -> Void
var modify: (ViewStorage, WidgetData) -> Void
/// The wrapped view.
var content: AnyView
@ -23,7 +23,7 @@ struct AppearObserver: ConvenienceWidget {
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let storage = content.storage(data: data, type: type)
modify(storage)
modify(storage, data)
return storage
}
@ -50,10 +50,17 @@ extension AnyView {
/// 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 {
public func inspectOnAppear(_ closure: @escaping (ViewStorage, WidgetData) -> Void) -> AnyView {
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.
/// - Parameter closure: The function.
/// - Returns: A view.

View 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)
}
}

View File

@ -9,7 +9,7 @@
struct InspectorWrapper: ConvenienceWidget {
/// The custom code to edit the widget.
var modify: (ViewStorage, Bool) -> Void
var modify: (ViewStorage, WidgetData, Bool) -> Void
/// The wrapped view.
var content: AnyView
@ -22,9 +22,7 @@ struct InspectorWrapper: ConvenienceWidget {
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let storage = content.storage(data: data, type: type)
modify(storage, true)
return storage
content.storage(data: data, type: type)
}
/// Update the stored content.
@ -40,7 +38,7 @@ struct InspectorWrapper: ConvenienceWidget {
type: Data.Type
) where Data: ViewRenderData {
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.
/// - 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 {
public func inspect(_ modify: @escaping (ViewStorage, WidgetData, Bool) -> Void) -> AnyView {
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.
/// - Parameter onUpdate: The function.
/// - Returns: A view.

View 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) }
}
}
}

View File

@ -12,6 +12,8 @@ struct StateWrapper: ConvenienceWidget {
var content: () -> Body
/// The state information (from properties with the `State` wrapper).
var state: [String: StateProtocol] = [:]
/// The environment properties.
var environment: [String: any EnvironmentProtocol] = [:]
/// Initialize a `StateWrapper`.
/// - Parameter content: The view content.
@ -23,9 +25,15 @@ struct StateWrapper: ConvenienceWidget {
/// - Parameters:
/// - content: The view content.
/// - 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.state = state
self.environment = environment
}
/// Update a view storage.
@ -51,6 +59,7 @@ struct StateWrapper: ConvenienceWidget {
property.value.content.update = false
}
}
assignEnvironment(data: data)
guard let storage = storage.content[.mainContent]?.first else {
return
}
@ -66,6 +75,7 @@ struct StateWrapper: ConvenienceWidget {
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
assignEnvironment(data: data)
let content = content().storage(data: data, type: type)
let storage = ViewStorage(content.pointer, content: [.mainContent: [content]])
storage.state = state
@ -75,4 +85,12 @@ struct StateWrapper: ConvenienceWidget {
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
View File

@ -0,0 +1,2 @@
add_subdirectory(SampleBackends)
add_subdirectory(DemoApp)

View 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
)

View File

@ -23,6 +23,7 @@ struct DemoApp: App {
var scene: Scene {
Backend1.Window("main", spawn: 1) {
DemoView(app: app)
.environment("test", data: 5)
}
}
@ -31,6 +32,8 @@ struct DemoApp: App {
struct DemoView: View {
@State private var model = TestModel()
@Environment("test")
private var test: Int?
var app: any AppStorage
let condition = false
@ -46,6 +49,7 @@ struct DemoView: View {
app.addSceneElement("main")
}
}
.onAppear { print(test ?? 0) }
}
TestView()
testContent

View 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
)