Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3e1894e80 | |||
| 682947abb0 | |||
| ba6a46f793 | |||
| 9a3760b24c | |||
| ebb68009ee | |||
| cd4e4dff9d | |||
| bd6c9d6f6b | |||
| d6a8f180ef | |||
| 80c430c546 | |||
| bb1d946b4a | |||
| 1a5cc43ef1 | |||
| 4b2127c55c | |||
| bc03112d2d |
@ -4,9 +4,21 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
Deploy:
|
||||||
runs-on: david-macbook
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: macos-14
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build Docs
|
- name: Build Docs
|
||||||
@ -19,16 +31,15 @@ jobs:
|
|||||||
xcrun docc process-archive transform-for-static-hosting \
|
xcrun docc process-archive transform-for-static-hosting \
|
||||||
"$PWD/.derivedData/Build/Products/Debug/Meta.doccarchive" \
|
"$PWD/.derivedData/Build/Products/Debug/Meta.doccarchive" \
|
||||||
--output-path "docs" \
|
--output-path "docs" \
|
||||||
--hosting-base-path "/"
|
--hosting-base-path "Meta"
|
||||||
- name: Modify Docs
|
- name: Modify Docs
|
||||||
run: |
|
run: |
|
||||||
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>" > 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 Artifact
|
||||||
uses: wangyucode/sftp-upload-action@v2.0.4
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
host: 'volans.uberspace.de'
|
path: 'docs'
|
||||||
username: 'akforum'
|
- name: Deploy to GitHub Pages
|
||||||
password: ${{ secrets.password }}
|
id: deployment
|
||||||
localDir: 'docs'
|
uses: actions/deploy-pages@v4
|
||||||
remoteDir: '/var/www/virtual/akforum/meta.aparoksha.dev/'
|
|
||||||
@ -3,17 +3,17 @@ name: SwiftLint
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- '.gitea/workflows/swiftlint.yml'
|
- '.github/workflows/swiftlint.yml'
|
||||||
- '.swiftlint.yml'
|
- '.swiftlint.yml'
|
||||||
- '**/*.swift'
|
- '**/*.swift'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.gitea/workflows/swiftlint.yml'
|
- '.github/workflows/swiftlint.yml'
|
||||||
- '.swiftlint.yml'
|
- '.swiftlint.yml'
|
||||||
- '**/*.swift'
|
- '**/*.swift'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
paths:
|
paths:
|
||||||
- '.gitea/workflows/swiftlint.yml'
|
- '.github/workflows/swiftlint.yml'
|
||||||
- '.swiftlint.yml'
|
- '.swiftlint.yml'
|
||||||
- '**/*.swift'
|
- '**/*.swift'
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ jobs:
|
|||||||
SwiftLint:
|
SwiftLint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v1
|
||||||
- name: SwiftLint
|
- name: SwiftLint
|
||||||
uses: norio-nomura/action-swiftlint@3.2.1
|
uses: norio-nomura/action-swiftlint@3.2.1
|
||||||
with:
|
with:
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,4 +10,5 @@ DerivedData/
|
|||||||
/Package.resolved
|
/Package.resolved
|
||||||
.Ulysses-Group.plist
|
.Ulysses-Group.plist
|
||||||
/.docc-build
|
/.docc-build
|
||||||
|
/io.github.AparokshaUI.Generation.json
|
||||||
/.vscode
|
/.vscode
|
||||||
@ -94,6 +94,11 @@ disabled_rules:
|
|||||||
|
|
||||||
# Custom Rules
|
# Custom Rules
|
||||||
custom_rules:
|
custom_rules:
|
||||||
|
github_issue:
|
||||||
|
name: 'GitHub Issue'
|
||||||
|
regex: '//.(TODO|FIXME):.(?!.*(https://github\.com/AparokshaUI/Meta/issues/\d))'
|
||||||
|
message: 'The related GitHub issue must be included in a TODO or FIXME.'
|
||||||
|
severity: warning
|
||||||
|
|
||||||
fatal_error:
|
fatal_error:
|
||||||
name: 'Fatal Error'
|
name: 'Fatal Error'
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
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)
|
|
||||||
40
CONTRIBUTING.md
Normal file
40
CONTRIBUTING.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Thank you very much for taking the time for contributing to this project.
|
||||||
|
|
||||||
|
## Report a Bug
|
||||||
|
Just open a new issue on GitHub and describe the bug. It helps if your description is detailed. Thank you very much for your contribution!
|
||||||
|
|
||||||
|
## Suggest a New Feature
|
||||||
|
Just open a new issue on GitHub and describe the idea. Thank you very much for your contribution!
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
I am happy for every pull request, you do not have to follow these guidelines. However, it might help you to understand the project structure and make it easier for me to merge your pull request. Thank you very much for your contribution!
|
||||||
|
|
||||||
|
### 1. Fork & Clone this Project
|
||||||
|
Start by clicking on the `Fork` button at the top of the page. Then, clone this repository to your computer.
|
||||||
|
|
||||||
|
### 2. Open the Project
|
||||||
|
Open the project folder in GNOME Builder, Xcode or another IDE.
|
||||||
|
|
||||||
|
### 3. Understand the Project Structure
|
||||||
|
- The `README.md` file contains a description of the app or package.
|
||||||
|
- The `LICENSE.md` contains an MIT license.
|
||||||
|
- `CONTRIBUTING.md` is this file.
|
||||||
|
- Directory `Icons` that contains SVG files for the images used in the app and guides.
|
||||||
|
- `Sources` contains the source code of the project.
|
||||||
|
- `Model` contains the project's basis.
|
||||||
|
- `Data Flow` contains property wrappers and protocols required for managing the updates of a view.
|
||||||
|
- `Extensions` contains all the extensions of types that are not defined in this project.
|
||||||
|
- `User Interface` contains protocols and structures that are the basis of presenting content to the user.
|
||||||
|
- `View` contains structures that conform to the `AnyView` protocol.
|
||||||
|
- `Tests` contains a sample application using two sample backends for testing the package.
|
||||||
|
|
||||||
|
### 4. Edit the Code
|
||||||
|
Edit the code. If you add a new type, add documentation in the code.
|
||||||
|
|
||||||
|
### 5. Commit to the Fork
|
||||||
|
Commit and push the fork.
|
||||||
|
|
||||||
|
### 6. Pull Request
|
||||||
|
Open GitHub to submit a pull request. Thank you very much for your contribution!
|
||||||
@ -24,27 +24,17 @@ 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]
|
|
||||||
)
|
|
||||||
|
|||||||
10
README.md
10
README.md
@ -3,12 +3,12 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://meta.aparoksha.dev/">
|
<a href="https://aparokshaui.github.io/Meta/">
|
||||||
Documentation
|
Documentation
|
||||||
</a>
|
</a>
|
||||||
·
|
·
|
||||||
<a href="https://git.aparoksha.dev/aparoksha/meta">
|
<a href="https://github.com/AparokshaUI/Meta">
|
||||||
Code
|
GitHub
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ It knows the following layers of UI:
|
|||||||
- A scene element is a template for a container holding one or multiple views (e.g., a window).
|
- A scene element is a template for a container holding one or multiple views (e.g., a window).
|
||||||
- A view is a part of the actual UI inside a window, or another view.
|
- A view is a part of the actual UI inside a window, or another view.
|
||||||
|
|
||||||
Detailed information can be found in the [docs](https://meta.aparoksha.dev/).
|
Detailed information can be found in the [docs](https://aparokshaui.github.io/Meta/).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ Follow those steps if you want to create a UI framework.
|
|||||||
2. Open the `Package.swift` file.
|
2. Open the `Package.swift` file.
|
||||||
3. Into the `Package` initializer, under `dependencies`, paste:
|
3. Into the `Package` initializer, under `dependencies`, paste:
|
||||||
```swift
|
```swift
|
||||||
.package(url: "https://git.aparoksha.dev/aparoksha/meta", from: "0.1.0")
|
.package(url: "https://github.com/AparokshaUI/Meta", from: "0.1.0")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@ -7,7 +7,7 @@ Multiple UI frameworks can be used in the same code, but the selection of the fr
|
|||||||
A backend is a Swift package. It is a fully functional UI framework on its own, based on the principles of <doc:StateConcept> and <doc:DeclarativeDesign>.
|
A backend is a Swift package. It is a fully functional UI framework on its own, based on the principles of <doc:StateConcept> and <doc:DeclarativeDesign>.
|
||||||
The backend defines an ``AppStorage`` reference type, which implements a function for running and quitting the app.
|
The backend defines an ``AppStorage`` reference type, which implements a function for running and quitting the app.
|
||||||
It implements at least one type conforming to ``SceneElement`` that can be added to an app's scene.
|
It implements at least one type conforming to ``SceneElement`` that can be added to an app's scene.
|
||||||
Most importantly, a backend usually provides various widgets.
|
Most importantly, a backend provides various widgets.
|
||||||
|
|
||||||
Widgets are special views conforming to the ``Widget`` protocol. While other types of views combine other views, as one can see in the articles <doc:DeclarativeDesign> and <doc:StateConcept>, widgets call the underlying UI framework in an imperative fashion. When creating a backend, determine the views available in the UI framework and provide a widget abstraction.
|
Widgets are special views conforming to the ``Widget`` protocol. While other types of views combine other views, as one can see in the articles <doc:DeclarativeDesign> and <doc:StateConcept>, widgets call the underlying UI framework in an imperative fashion. When creating a backend, determine the views available in the UI framework and provide a widget abstraction.
|
||||||
|
|
||||||
@ -24,7 +24,8 @@ import TermKitBackend
|
|||||||
@main
|
@main
|
||||||
struct Subtasks: App {
|
struct Subtasks: App {
|
||||||
|
|
||||||
let app: TermKitApp() // Render using the TermKitBackend
|
let id = "io.github.david_swift.Subtasks"
|
||||||
|
var app: TermKitApp! // Render using the TermKitBackend
|
||||||
|
|
||||||
var scene: Scene {
|
var scene: Scene {
|
||||||
Window {
|
Window {
|
||||||
@ -49,4 +50,4 @@ Pass the correct view render data type (``ViewRenderData``) containing the widge
|
|||||||
|
|
||||||
If some combinations of backends are often used, it might be sensible to create an umbrella backend.
|
If some combinations of backends are often used, it might be sensible to create an umbrella backend.
|
||||||
An umbrella backend is simply a collection of view and scene element definitions with support for a specific set of platforms.
|
An umbrella backend is simply a collection of view and scene element definitions with support for a specific set of platforms.
|
||||||
An alternative to the ``App`` protocol ensures that the right backend is selected on the right platform. Additionally, it might be sensible to allow the user to overwrite a platform's default selection with an environment variable.
|
An alternative to the ``App`` protocol ensures that the right backend is selected on the right platform.
|
||||||
|
|||||||
@ -86,7 +86,8 @@ The following code shows all of the available levels of UI for a typical desktop
|
|||||||
@main
|
@main
|
||||||
struct AwesomeApp: App { // The app (no DSL)
|
struct AwesomeApp: App { // The app (no DSL)
|
||||||
|
|
||||||
let app = SomeBackendApp()
|
let id = "io.github.david_swift.AwesomeApp"
|
||||||
|
var app: GenericDesktopApp!
|
||||||
|
|
||||||
var scene: Scene { // The scene DSL
|
var scene: Scene { // The scene DSL
|
||||||
Window("Awesome App") { // The view DSL
|
Window("Awesome App") { // The view DSL
|
||||||
@ -120,7 +121,8 @@ First, it is possible to create additional computed variables (such as `scene` a
|
|||||||
@main
|
@main
|
||||||
struct AwesomeApp: App {
|
struct AwesomeApp: App {
|
||||||
|
|
||||||
let app = SomeBackendApp()
|
let id = "io.github.david_swift.AwesomeApp"
|
||||||
|
var app: GenericDesktopApp!
|
||||||
|
|
||||||
var scene: Scene {
|
var scene: Scene {
|
||||||
MenuBar {
|
MenuBar {
|
||||||
|
|||||||
@ -16,7 +16,8 @@ An app is built around data it can read and modify. This data is called state. A
|
|||||||
@main
|
@main
|
||||||
struct CounterApp: App {
|
struct CounterApp: App {
|
||||||
|
|
||||||
let app = SomeBackendApp()
|
let id = "io.github.david_swift.CounterApp"
|
||||||
|
var app: SomePlatformApp!
|
||||||
|
|
||||||
@State private var count = 0 // Initialize state
|
@State private var count = 0 // Initialize state
|
||||||
|
|
||||||
@ -49,7 +50,8 @@ For more complex operations, computed variables can be helpful.
|
|||||||
@main
|
@main
|
||||||
struct CounterApp: App {
|
struct CounterApp: App {
|
||||||
|
|
||||||
let app = SomeBackendApp()
|
let id = "io.github.david_swift.CounterApp"
|
||||||
|
var app: SomePlatformApp!
|
||||||
|
|
||||||
@State private var count = 0
|
@State private var count = 0
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ Add it as a dependency to your package manifest.
|
|||||||
If there is no backend for the UI framework available, you can create one yourself. Help is available under <doc:CreateBackend>.
|
If there is no backend for the UI framework available, you can create one yourself. Help is available under <doc:CreateBackend>.
|
||||||
If you need a specific combination of platforms, creating an umbrella backend may be a solution. Find more information under <doc:Backends>.
|
If you need a specific combination of platforms, creating an umbrella backend may be a solution. Find more information under <doc:Backends>.
|
||||||
|
|
||||||
In this tutorial, [TermKitBackend](https://git.aparoksha.dev/david-swift/term-kit-backend) will be used as a sample backend.
|
In this tutorial, [TermKitBackend](https://github.com/david-swift/TermKitBackend) will be used as a sample backend.
|
||||||
|
|
||||||
## Create the User Interface
|
## Create the User Interface
|
||||||
|
|
||||||
@ -22,7 +22,8 @@ import TermKitBackend
|
|||||||
@main
|
@main
|
||||||
struct AppName: App {
|
struct AppName: App {
|
||||||
|
|
||||||
let app = TermKitApp()
|
let id = "com.example.AppName"
|
||||||
|
var app: TermKitApp!
|
||||||
|
|
||||||
var scene: Scene {
|
var scene: Scene {
|
||||||
Window {
|
Window {
|
||||||
@ -33,7 +34,7 @@ struct AppName: App {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The app property holds the platform-specific application object.
|
The `id` property holds what is known as the bundle identifier on Apple platforms and as the Application ID on GNOME: a reverse DNS style identifier.
|
||||||
Replace the type of the `app` property with the app storage type of your backend. Find the type in the backend's documentation - it conforms to ``AppStorage`` and usually has the suffix "App".
|
Replace the type of the `app` property with the app storage type of your backend. Find the type in the backend's documentation - it conforms to ``AppStorage`` and usually has the suffix "App".
|
||||||
|
|
||||||
Fill `scene` with the UI definition. More information about the UI elements and the organization of the code is available under <doc:DeclarativeDesign>.
|
Fill `scene` with the UI definition. More information about the UI elements and the organization of the code is available under <doc:DeclarativeDesign>.
|
||||||
@ -59,10 +60,12 @@ import TermKitBackend
|
|||||||
@main
|
@main
|
||||||
struct AppName: App {
|
struct AppName: App {
|
||||||
|
|
||||||
|
let id = "com.example.AppName"
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
let app = MacApp(id: "dev.aparoksha.AppName")
|
var app: MacApp!
|
||||||
#else
|
#else
|
||||||
let app = AdwaitaApp(id: "dev.aparoksha.AppName")
|
var app: AdwaitaApp!
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var scene: Scene {
|
var scene: Scene {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ Learn how to implement a backend.
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
In this tutorial, [TermKitBackend](https://git.aparoksha.dev/david-swift/term-kit-backend) will be used as a sample backend to explain the elements of a backend.
|
In this tutorial, [TermKitBackend](https://github.com/david-swift/TermKitBackend) will be used as a sample backend to explain the elements of a backend.
|
||||||
General information can be found in the <doc:Backends> article.
|
General information can be found in the <doc:Backends> article.
|
||||||
|
|
||||||
## Package Manifest
|
## Package Manifest
|
||||||
@ -13,10 +13,6 @@ Set up a new Swift Package (`swift package init`).
|
|||||||
Add the _Meta_ package as well as other dependencies if required to the dependencies section in the manifest.
|
Add the _Meta_ package as well as other dependencies if required to the dependencies section in the manifest.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// swift-tools-version: 6.0
|
|
||||||
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "TermKitBackend",
|
name: "TermKitBackend",
|
||||||
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
|
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
|
||||||
@ -27,271 +23,65 @@ let package = Package(
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://git.aparoksha.dev/aparoksha/meta", from: "0.1.0"),
|
.package(url: "https://github.com/AparokshaUI/Meta", from: "0.1.0"),
|
||||||
.package(url: "https://github.com/david-swift/TermKit", branch: "main")
|
.package(url: "https://github.com/david-swift/TermKit", branch: "main")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "TermKitBackend",
|
name: "TermKitBackend",
|
||||||
dependencies: [
|
dependencies: ["TermKit", "Meta"]
|
||||||
"TermKit",
|
|
||||||
.product(name: "Meta", package: "meta")
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
],
|
]
|
||||||
swiftLanguageModes: [.v5]
|
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Backend-Specific Scene Type
|
## Backend-Specific Protocols
|
||||||
|
|
||||||
An app can contain scenes from multiple backends. To determine which scenes to render, a backend-specific scene protocol is required.
|
As mentioned in <doc:Backends>, the backend has to define a backend-specific scene element type.
|
||||||
Simply create a protocol which conforms to the ``SceneElement`` protocol.
|
Often, it is sensible to define a widget type for regular views.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// TermKitSceneElement.swift
|
|
||||||
|
|
||||||
import Meta
|
import Meta
|
||||||
|
|
||||||
public protocol TermKitSceneElement: SceneElement { }
|
public protocol TermKitSceneElement: SceneElement { }
|
||||||
```
|
|
||||||
|
|
||||||
## The App Storage
|
|
||||||
|
|
||||||
When creating an app, the backend is selected via the app storage (more information under <doc:CreateApp>).
|
|
||||||
Furthermore, it offers functions that allow the user to quit the app (``AppStorage/quit()``) and manage which scene elements (often windows) are visible (``AppStorage/addSceneElement(_:)`` or ``AppStorage/addWindow(_:)``, ``AppStorage/showSceneElement(_:)`` or ``AppStorage/showWindow(_:)``).
|
|
||||||
|
|
||||||
The ``AppStorage/run(setup:)`` method is used by the Meta framework to execute the application, where the `setup` parameter is a closure initializing the scene elements in the app's ``App/scene`` property, based on the initial setup function of your scene elements.
|
|
||||||
Creating scene elements will be covered later in this article.
|
|
||||||
|
|
||||||
Store additional properties in the app storage. You can fetch the data for the additional properties via the initializer of the app storage (such as the app's identifier).
|
|
||||||
The ``AppStorage/storage`` stores a standard app storage which has to be the same in every backend.
|
|
||||||
|
|
||||||
Add a typealias defining the ``AppStorage/SceneElementType`` to be the backend-specific scene type.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// TermKitApp.swift
|
|
||||||
|
|
||||||
@_exported import Meta // Export the Meta framework (only required in one file)
|
|
||||||
import TermKit
|
|
||||||
|
|
||||||
public class TermKitApp: AppStorage {
|
|
||||||
|
|
||||||
public typealias SceneElementType = TermKitSceneElement
|
|
||||||
|
|
||||||
public var storage: StandardAppStorage = .init()
|
|
||||||
|
|
||||||
public init() { } // Optionally, fetch additional data
|
|
||||||
|
|
||||||
public func run(setup: @escaping () -> Void) {
|
|
||||||
Application.prepare()
|
|
||||||
setup() // Always call the setup closure in the run function
|
|
||||||
Application.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func quit() {
|
|
||||||
Application.shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create a Scene Element
|
|
||||||
|
|
||||||
Each backend defines one or multiple scene elements.
|
|
||||||
A scene element is anything one level below the application.
|
|
||||||
Windows are a common type of scene elements, but there may be other types such as the menu bar on macOS.
|
|
||||||
|
|
||||||
Scene elements are structures conforming to ``SceneElement``.
|
|
||||||
Note that for elements where multiple copies of one type of scene element are _possible_, such as windows, the scene element represents the _type_ of elements, not the individual elements.
|
|
||||||
Add individual copies via the previously mentioned functions on app storages.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// Window.swift
|
|
||||||
|
|
||||||
import TermKit
|
|
||||||
|
|
||||||
public struct Window: TermKitSceneElement { // Use the backend-specific scene element type
|
|
||||||
|
|
||||||
public var id: String
|
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
``SceneElement/id`` is an identifier which uniquely identifies the _type_ of scene element. For instance, use an identifier "main" which represents main windows.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// ...
|
|
||||||
|
|
||||||
var title: String?
|
|
||||||
var content: Body
|
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Those are additional properties required for initializing the scene elements. Common examples are a title displayed in a window's title bar and the window's content.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// ...
|
|
||||||
|
|
||||||
public init(_ title: String? = nil, id: String = "main", @ViewBuilder content: () -> Body) { // @ViewBuilder enables the DSL to be used
|
|
||||||
self.id = id
|
|
||||||
self.title = title
|
|
||||||
self.content = content()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
A public initializer providing the properties with data.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// ...
|
|
||||||
|
|
||||||
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
|
|
||||||
app.storage.sceneStorage.append(container(app: app))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
The function ``SceneElement/setupInitialContainers(app:)`` is responsible for creating a certain amount of initial instances of the scene element.
|
|
||||||
By convention, if sensible, create one element if no further information is provided, but let the user specify the number in the initializer or via a modifier.
|
|
||||||
Append the scene element's container to the scene storage of the app object.
|
|
||||||
|
|
||||||
In this example, the number of instances cannot be customized as creating additional windows is not supported.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// ...
|
|
||||||
|
|
||||||
public func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage {
|
|
||||||
let win = TermKit.Window(title)
|
|
||||||
win.fill()
|
|
||||||
Application.top.addSubview(win)
|
|
||||||
let storage = SceneStorage(id: id, pointer: win) {
|
|
||||||
win.ensureFocus()
|
|
||||||
}
|
|
||||||
let viewStorage = content.storage(
|
|
||||||
data: .init(sceneStorage: storage, appStorage: app),
|
|
||||||
type: TermKitMainView.self
|
|
||||||
)
|
|
||||||
if let pointer = viewStorage.pointer as? TermKit.View {
|
|
||||||
win.addSubview(pointer)
|
|
||||||
}
|
|
||||||
storage.content = [.mainContent: [viewStorage]]
|
|
||||||
return storage
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Scene elements as well as widgets are based on two functions: a function for creating a new instance of the element, and a function for updating a certain instance of an element.
|
|
||||||
|
|
||||||
For scene elements, the function for creating an instance is called ``SceneElement/container(app:)``.
|
|
||||||
|
|
||||||
First, create the instance in the imperative framework (I will refer to it as the native representation).
|
|
||||||
Store the identifier and the native representation (``SceneStorage/pointer``) as well as a function which presents the scene element in a new ``SceneStorage`` object.
|
|
||||||
|
|
||||||
In most frameworks, you have to get a single native representation for the whole content view of a certain scene element or widget.
|
|
||||||
Whenever this is required, store a property of the type ``Body`` and initialize using the ``AnyView/storage(data:type:)`` type.
|
|
||||||
When working with views, you have to pass the backend-specific widget type to the methods. More information on working with widgets under [Widgets](#Widgets).
|
|
||||||
Store view storages in the scene storage (preferably via ``SceneStorage/content``) to access the storages when updating the view.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// ...
|
|
||||||
|
|
||||||
public func update<Storage>(
|
|
||||||
_ storage: SceneStorage,
|
|
||||||
app: Storage,
|
|
||||||
updateProperties: Bool
|
|
||||||
) where Storage: AppStorage {
|
|
||||||
guard let viewStorage = storage.content[.mainContent]?.first else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
content
|
|
||||||
.updateStorage(
|
|
||||||
viewStorage,
|
|
||||||
data: .init(sceneStorage: storage, appStorage: app),
|
|
||||||
updateProperties: updateProperties,
|
|
||||||
type: TermKitMainView.self
|
|
||||||
)
|
|
||||||
/*
|
|
||||||
// Update additional properties
|
|
||||||
guard updateProperties else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let previousState = storage.previousState as? Self
|
|
||||||
if previousState?.property != property { // Only if equatable
|
|
||||||
nativeRepresentation.doSomething(basedOn: property) // Update the native representation
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
Application.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
In this case, there is only one property which can be modified after creating the widget: the content.
|
|
||||||
Get the view storage from the scene storage and update the view using ``AnyView/updateStorage(_:data:updateProperties:type:)``.
|
|
||||||
|
|
||||||
For properties in general (in scene elements and widgets), follow two simple rules:
|
|
||||||
|
|
||||||
- Update properties in the native representation only if the `updateProperties` parameter is `true`. If state has changed in a parent view, this property will be set to `true`, otherwise, there is nothing to update. Furthermore, if (and only if) the type of the property is equatable, check whether the value has actually changed before updating.
|
|
||||||
- An exception are views: always update views when the parent widget or scene element gets updated. The reason is that child views are able to hold state properties themselves, even though parent views do not know about the change in the state values.
|
|
||||||
|
|
||||||
## Widgets
|
|
||||||
|
|
||||||
Widgets are the basic building blocks of all the elements a scene element can hold.
|
|
||||||
This includes classic views (such as buttons, text fields, etc.) as well as more limited views (such as menus).
|
|
||||||
|
|
||||||
Similarly to the backend-specific scene element protocol, create a view-type-specific protocol conforming to ``Widget`` (e.g. one for "regular" views, one for menus, etc.).
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// TermKitWidget.swift
|
|
||||||
|
|
||||||
public protocol TermKitWidget: Widget { }
|
public protocol TermKitWidget: Widget { }
|
||||||
```
|
```
|
||||||
|
|
||||||
The widget type (``TermKitMainView/WidgetType``) is not enough for working with view types.
|
## The Wrapper Widget
|
||||||
Each view type has to provide two special views (usually widgets):
|
|
||||||
|
|
||||||
- ``ViewRenderData/WrapperType`` is a type which is used when multiple views are used in a view body without explicitly specifying the container. If there is only a single element, it usually returns that element, otherwise, it usually aligns the elements vertically.
|
In this section, the widget type for regular views will be extended so that it can be used for rendering.
|
||||||
- ``ViewRenderData/EitherViewType`` allows Swift's `if`/`else` syntax to be used inside the bodies. While calling an either view widget like any other widget would work, Swift's `if`/`else` syntax allows more advanced syntax such as unwrapping optionals.
|
|
||||||
|
|
||||||
### The Wrapper Widget
|
With _Meta_, arrays of ``AnyView`` have to be able to be converted into a single widget.
|
||||||
|
This allows definitions such as the following one:
|
||||||
Let's create the wrapper type first. This illustrates the process of creating complex widgets. For simpler widgets, there is a more concise syntax available.
|
|
||||||
|
|
||||||
We will call the container type `VStack`. This is dervied from SwiftUI and aligns the elements vertically.
|
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// VStack.swift
|
Window {
|
||||||
|
Label("Hello")
|
||||||
|
Label("World")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a widget which arranges the child views next to each other (on most platforms, doing this vertically makes most sense).
|
||||||
|
It should conform to the platform-specific widget type as well as ``Wrapper``.
|
||||||
|
Read the comments for general information about creating widgets.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import Meta
|
||||||
import TermKit
|
import TermKit
|
||||||
|
|
||||||
public struct VStack: Wrapper, TermKitWidget {
|
public struct VStack: Wrapper, TermKitWidget {
|
||||||
|
|
||||||
var content: Body
|
var content: Body
|
||||||
|
|
||||||
public init(@ViewBuilder content: @escaping () -> Body) {
|
public init(@ViewBuilder content: @escaping () -> Body) { // Use functions and mark them with the result builder to allow the domain-specific language to be used
|
||||||
self.content = content()
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
A wrapper type has to conform to the ``Wrapper`` protocol.
|
|
||||||
The only requirement of this protocol is the ``Wrapper/init(content:)`` initializer.
|
|
||||||
|
|
||||||
Each widget conforms to the backend-specific widget protocol. As with scene elements, a widget can store "regular" properties as well as view bodies.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// ...
|
|
||||||
|
|
||||||
public func container<Data>(
|
public func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) -> ViewStorage where Data: ViewRenderData {
|
||||||
let storages = content.storages(data: data, type: type)
|
let storages = content.storages(data: data, type: type) // Get the storages of child views
|
||||||
if storages.count == 1 {
|
if storages.count == 1 {
|
||||||
return .init(storages[0].pointer, content: [.mainContent: storages])
|
return .init(storages[0].pointer, content: [.mainContent: storages])
|
||||||
}
|
}
|
||||||
@ -299,29 +89,14 @@ Each widget conforms to the backend-specific widget protocol. As with scene elem
|
|||||||
for (index, storage) in storages.enumerated() {
|
for (index, storage) in storages.enumerated() {
|
||||||
if let pointer = storage.pointer as? TermKit.View {
|
if let pointer = storage.pointer as? TermKit.View {
|
||||||
view.addSubview(pointer)
|
view.addSubview(pointer)
|
||||||
if let previous = (storages[safe: index - 1]?.pointer as? TermKit.View) {
|
if let previous = (storages[safe: index - 1]?.pointer as? TermKit.View) { // The pointer should be a TermKit view
|
||||||
pointer.y = .bottom(of: previous)
|
pointer.y = .bottom(of: previous)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return .init(view, content: [.mainContent: storages])
|
return .init(view, content: [.mainContent: storages]) // Save storages of child views in the parent's storage for view updates
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
The ``Widget/container(data:type:)`` method is the equivalent to ``SceneElement/container(app:)`` for widgets.
|
|
||||||
Initialize the native representation.
|
|
||||||
|
|
||||||
This is a very complex container function. The reason is that the wrapper is not allowed to use the convenience [storage](https://meta.aparoksha.dev/documentation/meta/anyview/storage(data:type:))
|
|
||||||
function as this method uses the container widget to "combine" multiple views. Therefore, it is important to add the individual views to the native representation.
|
|
||||||
If there is only one item (note that this is checked via `storages`, not via the `content` property, as there might be items from other backends), return this item instead.
|
|
||||||
|
|
||||||
Store the storages for child views in the view storage's `content` property. You will access it in the updating function to update the content.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// ...
|
|
||||||
|
|
||||||
public func update<Data>(
|
public func update<Data>(
|
||||||
_ storage: ViewStorage,
|
_ storage: ViewStorage,
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
@ -331,299 +106,80 @@ Store the storages for child views in the view storage's `content` property. You
|
|||||||
guard let storages = storage.content[.mainContent] else {
|
guard let storages = storage.content[.mainContent] else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content.update(storages, data: data, updateProperties: updateProperties, type: type)
|
content.update(storages, data: data, updateProperties: updateProperties, type: type) // Update the storages of child views
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In the update function, the storages are updated based on the current state of the content.
|
|
||||||
|
|
||||||
### The Either View
|
|
||||||
|
|
||||||
The either view allows standard Swift `if`/`else` syntax to be used in a view body.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// EitherView.swift
|
|
||||||
|
|
||||||
import TermKit
|
|
||||||
|
|
||||||
public struct EitherView: TermKitWidget, Meta.EitherView {
|
|
||||||
|
|
||||||
var condition: Bool
|
|
||||||
var view1: Body
|
|
||||||
var view2: Body
|
|
||||||
|
|
||||||
public init(_ condition: Bool, @ViewBuilder view1: () -> Body, @ViewBuilder else view2: () -> Body) {
|
|
||||||
self.condition = condition
|
|
||||||
self.view1 = view1()
|
|
||||||
self.view2 = view2()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
As always, store relevant properties in the widget.
|
|
||||||
For the ``EitherView``, it is again the initializer that has to match a requirement (``EitherView/init(_:view1:else:)``).
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// ...
|
|
||||||
|
|
||||||
public func container<Data>(
|
|
||||||
data: WidgetData,
|
|
||||||
type: Data.Type
|
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
|
||||||
let view = TermKit.View()
|
|
||||||
return .init(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
The either view has a quite complicated structure as well (it will get way easier for "normal" widgets, I promise).
|
|
||||||
Normally, you would initialize all your content storages in the container function (as in the wrapper widget).
|
|
||||||
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
|
|
||||||
// ...
|
|
||||||
|
|
||||||
public func update<Data>(
|
|
||||||
_ storage: ViewStorage,
|
|
||||||
data: WidgetData,
|
|
||||||
updateProperties: Bool,
|
|
||||||
type: Data.Type
|
|
||||||
) where Data: ViewRenderData {
|
|
||||||
guard let parent = storage.pointer as? TermKit.View else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let view: TermKit.View?
|
|
||||||
let body = condition ? view1 : view2
|
|
||||||
if let content = storage.content[condition.description]?.first {
|
|
||||||
body.updateStorage(content, data: data, updateProperties: updateProperties, type: type)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
if let view, (storage.previousState as? Self)?.condition != condition {
|
|
||||||
parent.removeAllSubviews()
|
|
||||||
parent.addSubview(view)
|
|
||||||
}
|
|
||||||
storage.previousState = self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If the storage for the current condition has already been initialized, this method calls the children's update function.
|
### Correct Updating
|
||||||
Otherwise, it is initialized. Here, you can see how the convenient ``AnyView/storage(data:type:)`` and ``AnyView/updateStorage(_:data:updateProperties:type:)`` is being used for this.
|
|
||||||
|
|
||||||
Assign the correct view to the native representation of the either view if the condition has changed.
|
Note that the type of the ``ViewStorage/pointer`` differs from backend to backend.
|
||||||
|
It is a reference to the widget in the original UI framework.
|
||||||
|
|
||||||
### The View Render Data
|
In the update method, update properties of a widget (such as a button's label) only when the `updateProperties` parameter is `true`.
|
||||||
|
It indicates that a state variable (see <doc:StateConcept>) of an ancestor view has been updated.
|
||||||
|
If state doesn't change, it is impossible for the UI to change.
|
||||||
|
However, consider the following exceptions:
|
||||||
|
|
||||||
Each view context (regular views, menus, etc.) has its own view render data type. You could already see it passed to widgets as the `type` parameter.
|
- _Always_ update view content (using ``AnyView/updateStorage(_:data:updateProperties:type:)`` or ``Swift/Array/storages(data:type:)``). Child views may contain own state.
|
||||||
This allows widgets to behave differently based on the context (in case this is required). Don't forget to make the widget conform to the widget protocol of all the view contexts it supports!
|
- _Always_ update closures (such as the action of a button widget). They may contain reference to state which is updated whenever a view update takes place.
|
||||||
|
- _Always_ update bindings. As one can see when looking at ``Binding/init(get:set:)``, they contain two closures which, in most cases, contain a reference to state.
|
||||||
|
|
||||||
Let's create the view context type for our main view context:
|
### The Render Data Type
|
||||||
|
|
||||||
|
Now, define a view render data type for the main views.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// TermKitMainView.swift
|
public enum MainViewType: ViewRenderData {
|
||||||
|
|
||||||
public enum TermKitMainView: ViewRenderData {
|
|
||||||
|
|
||||||
public typealias WidgetType = TermKitWidget
|
public typealias WidgetType = TermKitWidget
|
||||||
public typealias WrapperType = VStack
|
public typealias WrapperType = VStack
|
||||||
public typealias EitherViewType = EitherView
|
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### A Regular Widget
|
It is possible to have multiple view render data types in one backend for different situations.
|
||||||
|
As an example, you could add another type for menus.
|
||||||
|
|
||||||
Regular widgets are much simpler to implement. Here, we will implement a simple label widget.
|
## The App Storage
|
||||||
|
|
||||||
While one could use the `container`/`update` methods to implement any widgets, for most wigdets, it might be more sensible to use a more declarative approach.
|
An app storage object in the app definition determines which backend to use for rendering.
|
||||||
One can set the ``Widget/initializeWidget()-9ut4i`` function instead and mark the properties as widget properties with the following property wrappers:
|
Therefore, it must contain information about the scene element.
|
||||||
|
|
||||||
- ``ViewProperty`` for child views
|
Additionally, the function for executing the app is defined on the object, allowing you to put the setup of the UI into the correct context.
|
||||||
- ``BindingProperty`` for bindings (see <doc:StateConcept>)
|
The quit funtion should terminate the app.
|
||||||
- ``Property`` for other properties (labels, closures, etc.)
|
|
||||||
|
|
||||||
First, create the widget's structure without any backend logic:
|
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Label.swift
|
@_exported import Meta // Export the Meta package
|
||||||
|
|
||||||
import TermKit
|
import TermKit
|
||||||
|
|
||||||
public struct Label: TermKitWidget {
|
public class TermKitApp: AppStorage {
|
||||||
|
|
||||||
var label: String
|
public typealias SceneElementType = TermKitSceneElement
|
||||||
|
|
||||||
public init(_ label: String) {
|
public var storage: StandardAppStorage = .init()
|
||||||
self.label = label
|
|
||||||
|
public required init(id: String) { }
|
||||||
|
|
||||||
|
public func run(setup: @escaping () -> Void) {
|
||||||
|
Application.prepare()
|
||||||
|
setup()
|
||||||
|
Application.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
public func quit() {
|
||||||
```
|
Application.shutdown()
|
||||||
|
|
||||||
Then, create a method for initializing the native representation.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// Label.swift
|
|
||||||
|
|
||||||
import TermKit
|
|
||||||
|
|
||||||
public struct Label: TermKitWidget {
|
|
||||||
|
|
||||||
var label: String
|
|
||||||
|
|
||||||
public init(_ label: String) {
|
|
||||||
self.label = label
|
|
||||||
}
|
|
||||||
|
|
||||||
public func initializeWidget() -> Any {
|
|
||||||
TermKit.Label(label) // You woudn't set properties here usually, but the label initializer needs a label already
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, mark the `label` property with the ``Property`` property wrapper to provide a closure for "translating" the declarative representation into an imperative program.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// Label.swift
|
|
||||||
|
|
||||||
import TermKit
|
|
||||||
|
|
||||||
public struct Label: TermKitWidget {
|
|
||||||
|
|
||||||
@Property(set: { $0.text = $1 }, pointer: TermKit.Label.self) // This is a property
|
|
||||||
var label = ""
|
|
||||||
|
|
||||||
public init(_ label: String) {
|
|
||||||
self.label = label
|
|
||||||
}
|
|
||||||
|
|
||||||
public func initializeWidget() -> Any {
|
|
||||||
TermKit.Label(label)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is already a functioning label widget!
|
|
||||||
|
|
||||||
### The Binding Property Wrapper
|
|
||||||
|
|
||||||
Whenever a property needs to allow two-way traffic (let the parent view as well as the widget itself modify the property), use a ``Binding``.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// Checkbox.swift
|
|
||||||
|
|
||||||
import TermKit
|
|
||||||
|
|
||||||
public struct Checkbox: TermKitWidget {
|
|
||||||
|
|
||||||
@Property(set: { $0.text = $1 }, pointer: TermKit.Checkbox.self)
|
|
||||||
var label: String
|
|
||||||
var isOn: Binding<Bool> // The binding property
|
|
||||||
|
|
||||||
public init(_ label: String, isOn: Binding<Bool>) {
|
|
||||||
self.label = label
|
|
||||||
self.isOn = isOn
|
|
||||||
}
|
|
||||||
|
|
||||||
public func initializeWidget() -> Any {
|
|
||||||
TermKit.Label(label)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The ``BindingProperty`` initializer accepts two closures.
|
|
||||||
|
|
||||||
The first one, `observe`, is called when setting up the widget (in the `container` method), and should connect the binding to a callback.
|
|
||||||
The second one, `set`, is equivalent to the closure with the same name for ``Property``.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
// Checkbox.swift
|
|
||||||
|
|
||||||
import TermKit
|
|
||||||
|
|
||||||
public struct Checkbox: TermKitWidget {
|
|
||||||
|
|
||||||
@Property(set: { $0.text = $1 }, pointer: TermKit.Checkbox.self)
|
|
||||||
var label: String
|
|
||||||
@BindingProperty(
|
|
||||||
observe: { box, value in box.toggled = { value.wrappedValue = $0.checked } },
|
|
||||||
set: { $0.checked = $1 },
|
|
||||||
pointer: TermKit.Checkbox.self
|
|
||||||
)
|
|
||||||
var isOn: Binding<Bool>
|
|
||||||
|
|
||||||
public init(_ label: String, isOn: Binding<Bool>) {
|
|
||||||
self.label = label
|
|
||||||
self.isOn = isOn
|
|
||||||
}
|
|
||||||
|
|
||||||
public func initializeWidget() -> Any {
|
|
||||||
TermKit.Label(label)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### View Properties
|
|
||||||
|
|
||||||
View properties are properties of the type ``Body``.
|
|
||||||
The `set` closure will be called only in the `container` method.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
/// Frame.swift
|
|
||||||
|
|
||||||
import TermKit
|
|
||||||
|
|
||||||
public struct Frame: TermKitWidget {
|
|
||||||
|
|
||||||
@ViewProperty(
|
|
||||||
set: { $0.addSubview($1) },
|
|
||||||
pointer: TermKit.Frame.self,
|
|
||||||
subview: TermKit.View.self,
|
|
||||||
context: MainViewContext.self
|
|
||||||
)
|
|
||||||
var view: Body
|
|
||||||
|
|
||||||
public init(@ViewBuilder content: @escaping () -> Body) { // Use the view builder
|
|
||||||
self.view = content()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func initializeWidget() -> Any {
|
|
||||||
TermKit.Frame()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Define the type of the view context with the `context` property.
|
## Next Steps
|
||||||
|
|
||||||
Remember not to use this property wrapper in the wrapper widget.
|
Now, you can start implementing scene elements (windows or other "top-level containers"), and views.
|
||||||
|
Remember following the instructions for correct updating above for all of the UI element types.
|
||||||
### Complex Widgets
|
|
||||||
|
|
||||||
More complex widgets (we have already created the two "special" widgets, the wrapper and either view widget, using this method)
|
|
||||||
should be created using the ``Widget/container(data:type:)`` and ``Widget/update(_:data:updateProperties:type:)`` methods.
|
|
||||||
|
|
||||||
## Create Apps
|
|
||||||
|
|
||||||
Now that you have a backend with some scene elements and widgets, learn how to create an app under <doc:CreateApp>.
|
|
||||||
|
|
||||||
Find other backends on the [Aparoksha website](https://www.aparoksha.dev/backends/) or [forums](https://forums.aparoksha.dev/t/projects).
|
|
||||||
|
|
||||||
|
If you still have questions, browse code in the [TermKitBackend repository](https://github.com/david-swift/TermKitBackend) or ask a question in the [discussions](https://github.com/AparokshaUI/Meta/discussions). Feedback on the documentation is appreciated!
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"button-text": "#ffffff",
|
"button-text": "#ffffff",
|
||||||
"header": "#7f313b",
|
"header": "#7f313b",
|
||||||
"documentation-intro-accent": "var(--color-header)",
|
"documentation-intro-accent": "var(--color-header)",
|
||||||
|
"documentation-intro-fill": "radial-gradient(circle at top, var(--color-header) 30%, #000 100%)",
|
||||||
"link": "#ea3358",
|
"link": "#ea3358",
|
||||||
"nav-link-color": "#ea3358",
|
"nav-link-color": "#ea3358",
|
||||||
"nav-dark-link-color": "#ea3358",
|
"nav-dark-link-color": "#ea3358",
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
/// ```
|
/// ```
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
@dynamicMemberLookup
|
@dynamicMemberLookup
|
||||||
public struct Binding<Value> {
|
public struct Binding<Value>: Sendable where Value: Sendable {
|
||||||
|
|
||||||
/// The value.
|
/// The value.
|
||||||
public var wrappedValue: Value {
|
public var wrappedValue: Value {
|
||||||
@ -64,11 +64,11 @@ public struct Binding<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The closure for getting the value.
|
/// The closure for getting the value.
|
||||||
private let getValue: () -> Value
|
private let getValue: @Sendable () -> Value
|
||||||
/// The closure for settings the value.
|
/// The closure for settings the value.
|
||||||
private let setValue: (Value) -> Void
|
private let setValue: @Sendable (Value) -> Void
|
||||||
/// Handlers observing whether the binding changes.
|
/// Handlers observing whether the binding changes.
|
||||||
private var handlers: [(Value) -> Void] = []
|
private var handlers: [@Sendable (Value) -> Void] = []
|
||||||
|
|
||||||
/// Get a property of any content of a `Binding` as a `Binding`.
|
/// Get a property of any content of a `Binding` as a `Binding`.
|
||||||
/// - Parameter keyPath: The path to the member.
|
/// - Parameter keyPath: The path to the member.
|
||||||
@ -85,7 +85,7 @@ public struct Binding<Value> {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - get: The closure for getting the value.
|
/// - get: The closure for getting the value.
|
||||||
/// - set: The closure for setting the value.
|
/// - set: The closure for setting the value.
|
||||||
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) {
|
public init(get: @Sendable @escaping () -> Value, set: @Sendable @escaping (Value) -> Void) {
|
||||||
self.getValue = get
|
self.getValue = get
|
||||||
self.setValue = set
|
self.setValue = set
|
||||||
}
|
}
|
||||||
@ -104,7 +104,7 @@ public struct Binding<Value> {
|
|||||||
/// Observe whether data is changed over this binding.
|
/// Observe whether data is changed over this binding.
|
||||||
/// - Parameter handler: The handler.
|
/// - Parameter handler: The handler.
|
||||||
/// - Returns: The binding.
|
/// - Returns: The binding.
|
||||||
public func onSet(_ handler: @escaping (Value) -> Void) -> Self {
|
public func onSet(_ handler: @Sendable @escaping (Value) -> Void) -> Self {
|
||||||
var newSelf = self
|
var newSelf = self
|
||||||
newSelf.handlers.append(handler)
|
newSelf.handlers.append(handler)
|
||||||
return newSelf
|
return newSelf
|
||||||
@ -113,7 +113,7 @@ public struct Binding<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extend bindings.
|
/// Extend bindings.
|
||||||
extension Binding where Value: MutableCollection {
|
extension Binding where Value: MutableCollection, Value.Index: Sendable, Value.Element: Sendable {
|
||||||
|
|
||||||
/// Get a child at a certain index of the array as a binding.
|
/// Get a child at a certain index of the array as a binding.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@ -136,7 +136,8 @@ extension Binding where Value: MutableCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extend bindings.
|
/// Extend bindings.
|
||||||
extension Binding where Value: MutableCollection, Value.Element: Identifiable {
|
extension Binding
|
||||||
|
where Value: MutableCollection, Value.Element: Identifiable, Value.Index: Sendable, Value.Element: Sendable {
|
||||||
|
|
||||||
/// Get a child of the array with a certain id as a binding.
|
/// Get a child of the array with a certain id as a binding.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 }
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -58,7 +58,7 @@ public protocol Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Data about a model's state value.
|
/// Data about a model's state value.
|
||||||
public struct ModelData {
|
public struct ModelData: Sendable {
|
||||||
|
|
||||||
/// The state value's identifier.
|
/// The state value's identifier.
|
||||||
var storage: StateContent.Storage
|
var storage: StateContent.Storage
|
||||||
@ -67,23 +67,8 @@ public struct ModelData {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extend the model.
|
|
||||||
extension Model {
|
extension Model {
|
||||||
|
|
||||||
/// Get the value as a binding.
|
|
||||||
public var binding: Binding<Self> {
|
|
||||||
.init {
|
|
||||||
getModel()
|
|
||||||
} set: { newValue in
|
|
||||||
guard let data = model else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.storage.value = newValue
|
|
||||||
data.storage.update = true
|
|
||||||
StateManager.updateViews(force: data.force)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the model up.
|
/// Set the model up.
|
||||||
///
|
///
|
||||||
/// At the point this function gets called, the model data is available.
|
/// At the point this function gets called, the model data is available.
|
||||||
@ -100,7 +85,9 @@ extension Model {
|
|||||||
setModel(&model)
|
setModel(&model)
|
||||||
data.storage.value = model
|
data.storage.value = model
|
||||||
data.storage.update = true
|
data.storage.update = true
|
||||||
StateManager.updateViews(force: data.force)
|
Task {
|
||||||
|
await StateManager.updateViews(force: data.force)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current version of the model.
|
/// Get the current version of the model.
|
||||||
@ -116,3 +103,23 @@ extension Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Model where Self: Sendable {
|
||||||
|
|
||||||
|
/// Get the value as a binding.
|
||||||
|
public var binding: Binding<Self> {
|
||||||
|
.init {
|
||||||
|
getModel()
|
||||||
|
} set: { newValue in
|
||||||
|
guard let data = model else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.storage.value = newValue
|
||||||
|
data.storage.update = true
|
||||||
|
Task {
|
||||||
|
await StateManager.updateViews(force: data.force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -8,12 +8,14 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// A type that signalizes an action.
|
/// A type that signalizes an action.
|
||||||
public struct Signal {
|
public struct Signal: Model, Sendable {
|
||||||
|
|
||||||
/// An action is signalized by toggling a boolean to `true` and back to `false`.
|
/// An action is signalized by toggling a boolean to `true` and back to `false`.
|
||||||
@State var boolean = false
|
var boolean = false
|
||||||
/// A signal has a unique identifier.
|
/// A signal has a unique identifier.
|
||||||
public let id: UUID = .init()
|
public let id: UUID = .init()
|
||||||
|
/// The model data.
|
||||||
|
public var model: ModelData?
|
||||||
|
|
||||||
/// Whether the action has caused an update.
|
/// Whether the action has caused an update.
|
||||||
public var update: Bool { boolean }
|
public var update: Bool { boolean }
|
||||||
@ -23,7 +25,11 @@ public struct Signal {
|
|||||||
|
|
||||||
/// Activate a signal.
|
/// Activate a signal.
|
||||||
public func signal() {
|
public func signal() {
|
||||||
boolean = true
|
setModel { $0.boolean = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destroy a signal.
|
||||||
|
mutating func destroySignal() {
|
||||||
boolean = false
|
boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import Foundation
|
|||||||
|
|
||||||
/// A property wrapper for properties in a view that should be stored throughout view updates.
|
/// A property wrapper for properties in a view that should be stored throughout view updates.
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
public struct State<Value>: StateProtocol {
|
public struct State<Value>: StateProtocol, Sendable where Value: Sendable {
|
||||||
|
|
||||||
/// Access the stored value. This updates the views when being changed.
|
/// Access the stored value. This updates the views when being changed.
|
||||||
public var wrappedValue: Value {
|
public var wrappedValue: Value {
|
||||||
@ -18,11 +18,10 @@ public struct State<Value>: StateProtocol {
|
|||||||
}
|
}
|
||||||
nonmutating set {
|
nonmutating set {
|
||||||
rawValue = newValue
|
rawValue = newValue
|
||||||
if !blockUpdates {
|
|
||||||
content.update = true
|
content.update = true
|
||||||
StateManager.updateViews(force: forceUpdates)
|
Task {
|
||||||
|
await StateManager.updateViews(force: forceUpdates)
|
||||||
}
|
}
|
||||||
writeValue?(newValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,16 +48,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 = false
|
var forceUpdates: Bool
|
||||||
|
|
||||||
/// 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: @Sendable () -> Value
|
||||||
|
|
||||||
/// Perform additional operations when the value changes.
|
|
||||||
var writeValue: ((Value) -> Void)?
|
|
||||||
|
|
||||||
/// The content.
|
/// The content.
|
||||||
let content: StateContent = .init()
|
let content: StateContent = .init()
|
||||||
@ -66,43 +59,13 @@ public struct State<Value>: StateProtocol {
|
|||||||
/// Initialize a property representing a state in the view with an autoclosure.
|
/// Initialize a property representing a state in the view with an autoclosure.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - wrappedValue: The wrapped value.
|
/// - wrappedValue: The wrapped value.
|
||||||
|
/// - id: An explicit identifier.
|
||||||
/// - 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.
|
||||||
public init(wrappedValue: @autoclosure @escaping () -> Value, forceUpdates: Bool = false) {
|
public init(wrappedValue: @Sendable @autoclosure @escaping () -> Value, forceUpdates: Bool = false) {
|
||||||
getInitialValue = wrappedValue
|
getInitialValue = wrappedValue
|
||||||
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.
|
|
||||||
/// - 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 e.g. to get data from the disk.
|
|
||||||
public init(
|
|
||||||
wrappedValue: @escaping () -> Value,
|
|
||||||
writeValue: ((Value) -> Void)? = nil,
|
|
||||||
forceUpdates: Bool = false,
|
|
||||||
blockUpdates: Bool = false
|
|
||||||
) {
|
|
||||||
getInitialValue = wrappedValue
|
|
||||||
self.writeValue = writeValue
|
|
||||||
self.forceUpdates = forceUpdates
|
|
||||||
self.blockUpdates = blockUpdates
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the initial value.
|
/// Get the initial value.
|
||||||
/// - Returns: The initial value.
|
/// - Returns: The initial value.
|
||||||
func initialValue() -> Value {
|
func initialValue() -> Value {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// A class storing the state's content.
|
/// A class storing the state's content.
|
||||||
class StateContent {
|
final class StateContent: @unchecked Sendable {
|
||||||
|
|
||||||
/// The storage.
|
/// The storage.
|
||||||
var storage: Storage?
|
var storage: Storage?
|
||||||
@ -39,7 +39,7 @@ class StateContent {
|
|||||||
init() { }
|
init() { }
|
||||||
|
|
||||||
/// A class storing the value.
|
/// A class storing the value.
|
||||||
class Storage {
|
class Storage: @unchecked Sendable {
|
||||||
|
|
||||||
/// The stored value.
|
/// The stored value.
|
||||||
var value: Any
|
var value: Any
|
||||||
|
|||||||
@ -8,29 +8,52 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// This type manages view updates.
|
/// This type manages view updates.
|
||||||
public enum StateManager {
|
@globalActor
|
||||||
|
public actor StateManager {
|
||||||
|
|
||||||
/// Whether to block updates in general.
|
/// Whether to block updates in general.
|
||||||
public static var blockUpdates = false
|
@StateManager static var blockAllUpdates = false
|
||||||
|
/// The application identifier.
|
||||||
|
static var appID: String?
|
||||||
/// The functions handling view updates.
|
/// The functions handling view updates.
|
||||||
static var updateHandlers: [(Bool) -> Void] = []
|
static var updateHandlers: [@Sendable (Bool) async -> Void] = []
|
||||||
|
/// The shared instance of the actor.
|
||||||
|
public static let shared = StateManager()
|
||||||
|
|
||||||
/// Update all of the views.
|
/// Update all of the views.
|
||||||
/// - Parameter force: Whether to force all views to update.
|
/// - Parameter force: Whether to force all views to update.
|
||||||
///
|
///
|
||||||
/// Nothing happens if ``StateManager/blockUpdates`` is true.
|
/// Nothing happens if ``StateManager/blockUpdates`` is true.
|
||||||
public static func updateViews(force: Bool = false) {
|
public static func updateViews(force: Bool = false) async {
|
||||||
if !blockUpdates {
|
if await !blockAllUpdates {
|
||||||
for handler in updateHandlers {
|
for handler in updateHandlers {
|
||||||
handler(force)
|
Task {
|
||||||
|
await handler(force)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a handler that is called when the user interface should update.
|
/// Add a handler that is called when the user interface should update.
|
||||||
/// - Parameter handler: The handler. The parameter defines whether the whole UI should be force updated.
|
/// - Parameter handler: The handler. The parameter defines whether the whole UI should be force updated.
|
||||||
static func addUpdateHandler(handler: @escaping (Bool) -> Void) {
|
static func addUpdateHandler(handler: @Sendable @escaping (Bool) async -> Void) {
|
||||||
updateHandlers.append(handler)
|
updateHandlers.append(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Block all updates.
|
||||||
|
///
|
||||||
|
/// The user interface will not respond to changes.
|
||||||
|
@StateManager
|
||||||
|
public static func blockUpdates() {
|
||||||
|
blockAllUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unblock all updates.
|
||||||
|
///
|
||||||
|
/// The user interface will respond to changes.
|
||||||
|
@StateManager
|
||||||
|
public static func unblockUpdates() {
|
||||||
|
blockAllUpdates = false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// An interface for accessing `State` without specifying the generic type.
|
/// An interface for accessing `State` without specifying the generic type.
|
||||||
protocol StateProtocol {
|
protocol StateProtocol: Sendable {
|
||||||
|
|
||||||
/// The state content.
|
/// The state content.
|
||||||
var content: StateContent { get }
|
var content: StateContent { get }
|
||||||
|
|||||||
@ -44,10 +44,10 @@ extension Array: AnyView where Element == AnyView {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
for (index, element) in filter({ $0.renderable(type: type, data: data) }).enumerated() {
|
for (index, element) in filter({ $0.renderable(type: type, data: data) }).enumerated() {
|
||||||
if let storage = storages[safe: index] {
|
if let storage = storages[safe: index] {
|
||||||
element
|
await element
|
||||||
.widget(data: data, type: type)
|
.widget(data: data, type: type)
|
||||||
.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
}
|
}
|
||||||
@ -62,9 +62,9 @@ extension Array: AnyView where Element == AnyView {
|
|||||||
public func storages<Data>(
|
public func storages<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> [ViewStorage] where Data: ViewRenderData {
|
) async -> [ViewStorage] where Data: ViewRenderData {
|
||||||
compactMap { view in
|
await compactMap { view in
|
||||||
view.renderable(type: type, data: data) ? view.storage(data: data, type: type) : nil
|
await view.renderable(type: type, data: data) ? view.storage(data: data, type: type) : nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +96,78 @@ extension Array {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the first element of the sequence that satisfies the given predicate.
|
||||||
|
/// - Parameter predicate: A closure that takes an element of the sequence as its argument
|
||||||
|
/// and returns a Boolean value indicating whether the element is a match.
|
||||||
|
/// - Returns: The first element of the sequence that satisfies `predicate`,
|
||||||
|
/// or `nil` if there is no element that satisfies `predicate`.
|
||||||
|
public func first(where predicate: (Element) async throws -> Bool) async rethrows -> Element? {
|
||||||
|
for element in self {
|
||||||
|
let matches = try await predicate(element)
|
||||||
|
if matches {
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the index of the first element of the sequence that satisfies the given predicate.
|
||||||
|
/// - Parameter predicate: A closure that takes an element of the sequence as its argument
|
||||||
|
/// and returns a Boolean value indicating whether the element is a match.
|
||||||
|
/// - Returns: The index of the first element of the sequence that satisfies `predicate`,
|
||||||
|
/// or `nil` if there is no element that satisfies `predicate`.
|
||||||
|
public func firstIndex(where predicate: (Element) async throws -> Bool) async rethrows -> Int? {
|
||||||
|
for (index, element) in enumerated() {
|
||||||
|
let matches = try await predicate(element)
|
||||||
|
if matches {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the last element of the sequence that satisfies the given predicate.
|
||||||
|
/// - Parameter predicate: A closure that takes an element of the sequence as its argument
|
||||||
|
/// and returns a Boolean value indicating whether the element is a match.
|
||||||
|
/// - Returns: The last element of the sequence that satisfies `predicate`,
|
||||||
|
/// or `nil` if there is no element that satisfies `predicate`.
|
||||||
|
public func last(where predicate: (Element) async throws -> Bool) async rethrows -> Element? {
|
||||||
|
try await reversed().first(where: predicate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate.
|
||||||
|
/// - Parameter predicate: A closure that takes an element of the sequence as its argument
|
||||||
|
/// and returns a Boolean value indicating whether the element is a match.
|
||||||
|
/// - Returns: Whether the sequence contains an element that satisfies `predicate`.
|
||||||
|
public func contains(where predicate: (Element) async throws -> Bool) async rethrows -> Bool {
|
||||||
|
try await first(where: predicate) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an array containing the results of mapping the given closure over the sequence’s elements.
|
||||||
|
/// Remove elements that are `nil`.
|
||||||
|
/// - Parameter transform: Transforms each element.
|
||||||
|
/// - Returns: The result.
|
||||||
|
public func compactMap<ElementOfResult>(
|
||||||
|
_ transform: (Element) async throws -> ElementOfResult?
|
||||||
|
) async rethrows -> [ElementOfResult] {
|
||||||
|
var result: [ElementOfResult] = []
|
||||||
|
for element in self {
|
||||||
|
if let element = try await transform(element) {
|
||||||
|
result.append(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an array containing the results of mapping the given closure over the sequence’s elements.
|
||||||
|
/// - Parameter transform: Transforms each element.
|
||||||
|
/// - Returns: The result.
|
||||||
|
public func map<ElementOfResult>(
|
||||||
|
_ transform: (Element) async throws -> ElementOfResult
|
||||||
|
) async rethrows -> [ElementOfResult] {
|
||||||
|
try await compactMap { try await transform($0) }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extend arrays.
|
/// Extend arrays.
|
||||||
|
|||||||
8
Sources/Model/Extensions/KeyPath.swift
Normal file
8
Sources/Model/Extensions/KeyPath.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// KeyPath.swift
|
||||||
|
// Meta
|
||||||
|
//
|
||||||
|
// Created by david-swift on 18.09.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension KeyPath: @retroactive @unchecked Sendable { }
|
||||||
8
Sources/Model/Extensions/OpaquePointer.swift
Normal file
8
Sources/Model/Extensions/OpaquePointer.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// OpaquePointer.swift
|
||||||
|
// Meta
|
||||||
|
//
|
||||||
|
// Created by david-swift on 29.09.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension OpaquePointer: @retroactive @unchecked Sendable { }
|
||||||
@ -11,7 +11,8 @@
|
|||||||
/// @main
|
/// @main
|
||||||
/// struct Test: App {
|
/// struct Test: App {
|
||||||
///
|
///
|
||||||
/// let app = BackendApp()
|
/// let id = "io.github.AparokshaUI.TestApp"
|
||||||
|
/// var app: BackendApp!
|
||||||
///
|
///
|
||||||
/// var scene: Scene {
|
/// var scene: Scene {
|
||||||
/// WindowScene()
|
/// WindowScene()
|
||||||
@ -20,15 +21,19 @@
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
public protocol App {
|
public protocol App: Sendable {
|
||||||
|
|
||||||
/// The app storage type.
|
/// The app storage type.
|
||||||
associatedtype Storage: AppStorage
|
associatedtype Storage: AppStorage
|
||||||
|
|
||||||
|
/// The app's application ID.
|
||||||
|
var id: String { get }
|
||||||
/// The app's scene.
|
/// The app's scene.
|
||||||
@SceneBuilder var scene: Scene { get }
|
@SceneBuilder var scene: Scene { get }
|
||||||
|
// swiftlint:disable implicitly_unwrapped_optional
|
||||||
/// The app storage.
|
/// The app storage.
|
||||||
var app: Storage { get }
|
var app: Storage! { get set }
|
||||||
|
// swiftlint:enable implicitly_unwrapped_optional
|
||||||
|
|
||||||
/// An app has to have an `init()` initializer.
|
/// An app has to have an `init()` initializer.
|
||||||
init()
|
init()
|
||||||
@ -45,7 +50,6 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,26 +58,32 @@ extension App {
|
|||||||
///
|
///
|
||||||
/// To run the app, call the ``AppStorage/run(setup:)`` function.
|
/// To run the app, call the ``AppStorage/run(setup:)`` function.
|
||||||
public static func setupApp() -> Self {
|
public static func setupApp() -> Self {
|
||||||
let appInstance = self.init()
|
var appInstance = self.init()
|
||||||
appInstance.app.storage.app = { appInstance }
|
appInstance.app = Storage(id: appInstance.id)
|
||||||
StateManager.addUpdateHandler { force in
|
StateManager.addUpdateHandler { [appInstance] force in
|
||||||
let updateProperties = force || appInstance.getState().contains { $0.value.content.update }
|
let updateProperties = force || appInstance.getState().contains { $0.value.content.update }
|
||||||
var removeIndices: [Int] = []
|
var removeIndices: [Int] = []
|
||||||
for (index, element) in appInstance.app.storage.sceneStorage.enumerated() {
|
for (index, element) in await appInstance.app.storage.sceneStorage.enumerated() {
|
||||||
if element.destroy {
|
if await element.destroy {
|
||||||
removeIndices.insert(index, at: 0)
|
removeIndices.insert(index, at: 0)
|
||||||
} else if let scene = appInstance.scene.first(
|
} else if let scene = await appInstance.scene.first(
|
||||||
where: { $0.id == element.id }
|
where: { await $0.id == element.id }
|
||||||
) as? Storage.SceneElementType as? SceneElement {
|
) as? Storage.SceneElementType as? SceneElement {
|
||||||
scene.update(element, app: appInstance.app, updateProperties: updateProperties)
|
scene.update(element, app: appInstance.app, updateProperties: updateProperties)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index in removeIndices {
|
for index in removeIndices {
|
||||||
appInstance.app.storage.sceneStorage.remove(at: index)
|
await appInstance.app.modifyStandardAppStorage { $0.sceneStorage.remove(at: index) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
StateManager.appID = appInstance.id
|
||||||
|
Task { [appInstance] in
|
||||||
let state = appInstance.getState()
|
let state = appInstance.getState()
|
||||||
appInstance.app.storage.stateStorage = state
|
await appInstance.app.modifyStandardAppStorage { storage in
|
||||||
|
storage.stateStorage = state
|
||||||
|
storage.app = { appInstance }
|
||||||
|
}
|
||||||
|
}
|
||||||
return appInstance
|
return appInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// The app storage protocol.
|
/// The app storage protocol.
|
||||||
public protocol AppStorage: AnyObject {
|
public protocol AppStorage: Actor, Sendable {
|
||||||
|
|
||||||
/// The type of scene elements (which should be backend-specific).
|
/// The type of scene elements (which should be backend-specific).
|
||||||
associatedtype SceneElementType
|
associatedtype SceneElementType
|
||||||
@ -14,12 +14,34 @@ public protocol AppStorage: AnyObject {
|
|||||||
/// The scene storage.
|
/// The scene storage.
|
||||||
var storage: StandardAppStorage { get set }
|
var storage: StandardAppStorage { get set }
|
||||||
|
|
||||||
|
/// Initialize the app storage.
|
||||||
|
/// - Parameters id: The app's identifier.
|
||||||
|
init(id: String)
|
||||||
|
|
||||||
/// Run the application.
|
/// Run the application.
|
||||||
/// - Parameter setup: A closure that is expected to be executed right at the beginning.
|
/// - Parameter setup: A closure that is expected to be executed right at the beginning.
|
||||||
func run(setup: @escaping () -> Void)
|
nonisolated func run(setup: @Sendable @escaping () -> Void)
|
||||||
|
|
||||||
/// Terminate the application.
|
/// Terminate the application.
|
||||||
func quit()
|
nonisolated func quit()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppStorage {
|
||||||
|
|
||||||
|
/// Modify the app storage.
|
||||||
|
/// - Parameter modify: The modifications.
|
||||||
|
func modifyStandardAppStorage(_ modify: (inout StandardAppStorage) -> Void) {
|
||||||
|
var copy = storage
|
||||||
|
modify(©)
|
||||||
|
self.storage = copy
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a scene.
|
||||||
|
/// - Parameter scene: The scene.
|
||||||
|
public func appendScene(_ scene: SceneStorage) {
|
||||||
|
modifyStandardAppStorage { $0.sceneStorage.append(scene) }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,35 +50,44 @@ 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) {
|
nonisolated public func showSceneElement(_ id: String) {
|
||||||
guard let storage = storage.sceneStorage.last(where: { $0.id == id && !$0.destroy }) else {
|
Task {
|
||||||
addSceneElement(id)
|
await storage.sceneStorage
|
||||||
StateManager.updateViews(force: true)
|
.last { scene in
|
||||||
return
|
let destroy = await scene.destroy
|
||||||
|
return await scene.id == id && !destroy
|
||||||
|
}?
|
||||||
|
.show() ?? addSceneElement(id)
|
||||||
}
|
}
|
||||||
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.
|
||||||
/// - Parameter id: The element's id.
|
/// - Parameter id: The element's id.
|
||||||
public func addSceneElement(_ id: String) {
|
nonisolated public func addSceneElement(_ id: String) {
|
||||||
|
Task {
|
||||||
|
await internalAddSceneElement(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new scene element with the content of the scene element with a certain id.
|
||||||
|
/// - Parameter id: The element's id.
|
||||||
|
func internalAddSceneElement(_ id: String) {
|
||||||
if let element = storage.app?().scene.last(where: { $0.id == id }) {
|
if let element = storage.app?().scene.last(where: { $0.id == id }) {
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Focus the window with a certain id (if supported). Create the window if it doesn't already exist.
|
/// Focus the window with a certain id (if supported). Create the window if it doesn't already exist.
|
||||||
/// - Parameter id: The window's id.
|
/// - Parameter id: The window's id.
|
||||||
public func showWindow(_ id: String) {
|
nonisolated public func showWindow(_ id: String) {
|
||||||
showSceneElement(id)
|
showSceneElement(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new window with the content of the window template with a certain id.
|
/// Add a new window with the content of the window template with a certain id.
|
||||||
/// - Parameter id: The window template's id.
|
/// - Parameter id: The window template's id.
|
||||||
public func addWindow(_ id: String) {
|
nonisolated public func addWindow(_ id: String) {
|
||||||
addSceneElement(id)
|
addSceneElement(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// The app storage protocol.
|
/// The app storage protocol.
|
||||||
public struct StandardAppStorage {
|
public struct StandardAppStorage: Sendable {
|
||||||
|
|
||||||
/// The scene storage.
|
/// The scene storage.
|
||||||
public var sceneStorage: [SceneStorage] = []
|
public var sceneStorage: [SceneStorage] = []
|
||||||
@ -15,7 +15,7 @@ public struct StandardAppStorage {
|
|||||||
var stateStorage: [String: StateProtocol] = [:]
|
var stateStorage: [String: StateProtocol] = [:]
|
||||||
|
|
||||||
/// The scene.
|
/// The scene.
|
||||||
var app: (() -> any App)?
|
var app: (@Sendable () -> any App)?
|
||||||
|
|
||||||
/// Initialize the standard app storage.
|
/// Initialize the standard app storage.
|
||||||
public init() { }
|
public init() { }
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// A structure conforming to `SceneElement` can be added to an app's `scene` property.
|
/// A structure conforming to `SceneElement` can be added to an app's `scene` property.
|
||||||
public protocol SceneElement {
|
public protocol SceneElement: Sendable {
|
||||||
|
|
||||||
/// The window type's identifier.
|
/// The window type's identifier.
|
||||||
var id: String { get }
|
var id: String { get }
|
||||||
|
|||||||
@ -6,22 +6,22 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// Store a reference to a rendered scene element in a view storage.
|
/// Store a reference to a rendered scene element in a view storage.
|
||||||
public class SceneStorage {
|
public actor SceneStorage {
|
||||||
|
|
||||||
/// The scene element's identifier.
|
/// The scene element's identifier.
|
||||||
public var id: String
|
public var id: String
|
||||||
/// The pointer.
|
/// The pointer.
|
||||||
///
|
///
|
||||||
/// It can be a C pointer, a Swift class, or other information depending on the backend.
|
/// It can be a C pointer, a Swift class, or other information depending on the backend.
|
||||||
public var pointer: Any?
|
public var pointer: Sendable?
|
||||||
/// The scene element's view content.
|
/// The scene element's view content.
|
||||||
public var content: [String: [ViewStorage]]
|
var content: [String: [ViewStorage]]
|
||||||
/// Various properties of a scene element.
|
/// Various properties of a scene element.
|
||||||
public var fields: [String: Any] = [:]
|
var fields: [String: Sendable] = [:]
|
||||||
/// Whether the reference to the window should disappear in the next update.
|
/// Whether the reference to the window should disappear in the next update.
|
||||||
public var destroy = false
|
public var destroy = false
|
||||||
/// Show the scene element (including moving into the foreground, if possible).
|
/// Show the scene element (including moving into the foreground, if possible).
|
||||||
public var show: () -> Void
|
public var show: @Sendable () -> Void
|
||||||
/// The previous state of the scene element.
|
/// The previous state of the scene element.
|
||||||
public var previousState: SceneElement?
|
public var previousState: SceneElement?
|
||||||
|
|
||||||
@ -43,9 +43,9 @@ public class SceneStorage {
|
|||||||
/// - show: Function called when the scene element should be displayed.
|
/// - show: Function called when the scene element should be displayed.
|
||||||
public init(
|
public init(
|
||||||
id: String,
|
id: String,
|
||||||
pointer: Any?,
|
pointer: Sendable?,
|
||||||
content: [String: [ViewStorage]] = [:],
|
content: [String: [ViewStorage]] = [:],
|
||||||
show: @escaping () -> Void
|
show: @Sendable @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.pointer = pointer
|
self.pointer = pointer
|
||||||
@ -53,4 +53,65 @@ public class SceneStorage {
|
|||||||
self.show = show
|
self.show = show
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the pointer.
|
||||||
|
/// - Parameter value: The new pointer.
|
||||||
|
public func setPointer(_ value: Sendable?) {
|
||||||
|
pointer = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the element of a certain field.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key.
|
||||||
|
/// - value: The field.
|
||||||
|
public func setField(key: String, value: Sendable) {
|
||||||
|
fields[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a certain field.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key.
|
||||||
|
public func removeField(key: String) {
|
||||||
|
fields.removeValue(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the element of a certain field.
|
||||||
|
/// - Parameter key: The key.
|
||||||
|
/// - Returns: The field.
|
||||||
|
public func getField(key: String) -> Sendable? {
|
||||||
|
fields[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the content elements under a certain key.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key.
|
||||||
|
/// - value: The content elements.
|
||||||
|
public func setContent(key: String, value: [ViewStorage]) {
|
||||||
|
content[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the content elements under a certain key.
|
||||||
|
/// - Parameter key: The key.
|
||||||
|
/// - Returns: The content elements.
|
||||||
|
public func getContent(key: String) -> [ViewStorage] {
|
||||||
|
content[key] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the previous state.
|
||||||
|
/// - Parameter state: The state.
|
||||||
|
public func setPreviousState(_ state: SceneElement?) {
|
||||||
|
previousState = state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set whether the scene will be destroyed.
|
||||||
|
/// - Parameter destroy: Whether the scene will be destroyed.
|
||||||
|
public func setDestroy(_ destroy: Bool) {
|
||||||
|
self.destroy = destroy
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the closure which shows the scene.
|
||||||
|
/// - Parameter show: The closure.
|
||||||
|
public func setShow(_ show: @Sendable @escaping () -> Void) {
|
||||||
|
self.show = show
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// The view type used for any form of a view.
|
/// The view type used for any form of a view.
|
||||||
public protocol AnyView {
|
public protocol AnyView: Sendable {
|
||||||
|
|
||||||
/// The view's content.
|
/// The view's content.
|
||||||
@ViewBuilder var viewContent: Body { get }
|
@ViewBuilder var viewContent: Body { get }
|
||||||
@ -38,8 +38,8 @@ extension AnyView {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
widget(data: data, type: type)
|
await widget(data: data, type: type)
|
||||||
.update(storage, data: data, updateProperties: updateProperties, type: type)
|
.update(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +51,8 @@ extension AnyView {
|
|||||||
public func storage<Data>(
|
public func storage<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) async -> ViewStorage where Data: ViewRenderData {
|
||||||
widget(data: data, type: type).container(data: data, type: type)
|
await widget(data: data, type: type).container(data: data, type: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrap the view into a widget.
|
/// Wrap the view into a widget.
|
||||||
|
|||||||
@ -6,8 +6,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// 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.
|
||||||
|
|||||||
@ -10,14 +10,14 @@
|
|||||||
/// This will be used if you do not provide a custom ``Widget/update(_:data:updateProperties:type:)`` method
|
/// This will be used if you do not provide a custom ``Widget/update(_:data:updateProperties:type:)`` method
|
||||||
/// or call the ``Widget/updateProperties(_:updateProperties:)`` method in your custom update method.
|
/// or call the ``Widget/updateProperties(_:updateProperties:)`` method in your custom update method.
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
public struct BindingProperty<Value, Pointer>: BindingPropertyProtocol {
|
public struct BindingProperty<Value, Pointer>: BindingPropertyProtocol where Value: Sendable {
|
||||||
|
|
||||||
/// The wrapped binding.
|
/// The wrapped binding.
|
||||||
public var wrappedValue: Binding<Value>
|
public var wrappedValue: Binding<Value>
|
||||||
/// Observe the UI element.
|
/// Observe the UI element.
|
||||||
var observe: (Pointer, Binding<Value>, ViewStorage) -> Void
|
var observe: @Sendable (Pointer, Binding<Value>, ViewStorage) async -> Void
|
||||||
/// Set the UI element's property.
|
/// Set the UI element's property.
|
||||||
var setValue: (Pointer, Value, ViewStorage) -> Void
|
var setValue: @Sendable (Pointer, Value, ViewStorage) async -> Void
|
||||||
|
|
||||||
/// Initialize a property.
|
/// Initialize a property.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@ -27,8 +27,8 @@ public struct BindingProperty<Value, Pointer>: BindingPropertyProtocol {
|
|||||||
/// - pointer: The type of the pointer.
|
/// - pointer: The type of the pointer.
|
||||||
public init(
|
public init(
|
||||||
wrappedValue: Binding<Value>,
|
wrappedValue: Binding<Value>,
|
||||||
observe: @escaping (Pointer, Binding<Value>, ViewStorage) -> Void,
|
observe: @Sendable @escaping (Pointer, Binding<Value>, ViewStorage) async -> Void,
|
||||||
set setValue: @escaping (Pointer, Value, ViewStorage) -> Void,
|
set setValue: @Sendable @escaping (Pointer, Value, ViewStorage) async -> Void,
|
||||||
pointer: Pointer.Type
|
pointer: Pointer.Type
|
||||||
) {
|
) {
|
||||||
self.wrappedValue = wrappedValue
|
self.wrappedValue = wrappedValue
|
||||||
@ -44,14 +44,14 @@ public struct BindingProperty<Value, Pointer>: BindingPropertyProtocol {
|
|||||||
/// - pointer: The type of the pointer.
|
/// - pointer: The type of the pointer.
|
||||||
public init(
|
public init(
|
||||||
wrappedValue: Binding<Value>,
|
wrappedValue: Binding<Value>,
|
||||||
observe: @escaping (Pointer, Binding<Value>) -> Void,
|
observe: @Sendable @escaping (Pointer, Binding<Value>) async -> Void,
|
||||||
set setValue: @escaping (Pointer, Value) -> Void,
|
set setValue: @Sendable @escaping (Pointer, Value) async -> Void,
|
||||||
pointer: Pointer.Type
|
pointer: Pointer.Type
|
||||||
) {
|
) {
|
||||||
self.init(
|
self.init(
|
||||||
wrappedValue: wrappedValue,
|
wrappedValue: wrappedValue,
|
||||||
observe: { pointer, value, _ in observe(pointer, value) },
|
observe: { pointer, value, _ in await observe(pointer, value) },
|
||||||
set: { pointer, value, _ in setValue(pointer, value) },
|
set: { pointer, value, _ in await setValue(pointer, value) },
|
||||||
pointer: pointer
|
pointer: pointer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -59,19 +59,19 @@ public struct BindingProperty<Value, Pointer>: BindingPropertyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The binding property protocol.
|
/// The binding property protocol.
|
||||||
protocol BindingPropertyProtocol {
|
protocol BindingPropertyProtocol: Sendable {
|
||||||
|
|
||||||
/// The binding's wrapped value.
|
/// The binding's wrapped value.
|
||||||
associatedtype Value
|
associatedtype Value: Sendable
|
||||||
/// The storage's pointer.
|
/// The storage's pointer.
|
||||||
associatedtype Pointer
|
associatedtype Pointer
|
||||||
|
|
||||||
/// The wrapped value.
|
/// The wrapped value.
|
||||||
var wrappedValue: Binding<Value> { get }
|
var wrappedValue: Binding<Value> { get }
|
||||||
/// Observe the property.
|
/// Observe the property.
|
||||||
var observe: (Pointer, Binding<Value>, ViewStorage) -> Void { get }
|
var observe: @Sendable (Pointer, Binding<Value>, ViewStorage) async -> Void { get }
|
||||||
/// Set the property.
|
/// Set the property.
|
||||||
var setValue: (Pointer, Value, ViewStorage) -> Void { get }
|
var setValue: @Sendable (Pointer, Value, ViewStorage) async -> Void { get }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,12 +84,12 @@ extension Widget {
|
|||||||
func setBindingProperty<Property>(
|
func setBindingProperty<Property>(
|
||||||
property: Property,
|
property: Property,
|
||||||
storage: ViewStorage
|
storage: ViewStorage
|
||||||
) where Property: BindingPropertyProtocol {
|
) async where Property: BindingPropertyProtocol {
|
||||||
if let optional = property.wrappedValue.wrappedValue as? any OptionalProtocol, optional.optionalValue == nil {
|
if let optional = property.wrappedValue.wrappedValue as? any OptionalProtocol, optional.optionalValue == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let pointer = storage.pointer as? Property.Pointer {
|
if let pointer = await storage.pointer as? Property.Pointer {
|
||||||
property.setValue(pointer, property.wrappedValue.wrappedValue, storage)
|
await property.setValue(pointer, property.wrappedValue.wrappedValue, storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,10 +10,10 @@
|
|||||||
/// This will be used if you do not provide a custom ``Widget/update(_:data:updateProperties:type:)`` method
|
/// This will be used if you do not provide a custom ``Widget/update(_:data:updateProperties:type:)`` method
|
||||||
/// or call the ``Widget/updateProperties(_:updateProperties:)`` method in your custom update method.
|
/// or call the ``Widget/updateProperties(_:updateProperties:)`` method in your custom update method.
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
public struct Property<Value, Pointer>: PropertyProtocol {
|
public struct Property<Value, Pointer>: PropertyProtocol where Value: Sendable {
|
||||||
|
|
||||||
/// The function applying the property to the UI.
|
/// The function applying the property to the UI.
|
||||||
public var setProperty: (Pointer, Value, ViewStorage) -> Void
|
public var setProperty: @Sendable (Pointer, Value, ViewStorage) async -> Void
|
||||||
/// The wrapped value.
|
/// The wrapped value.
|
||||||
public var wrappedValue: Value
|
public var wrappedValue: Value
|
||||||
/// The update strategy.
|
/// The update strategy.
|
||||||
@ -27,7 +27,7 @@ public struct Property<Value, Pointer>: PropertyProtocol {
|
|||||||
/// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases.
|
/// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases.
|
||||||
public init(
|
public init(
|
||||||
wrappedValue: Value,
|
wrappedValue: Value,
|
||||||
set setProperty: @escaping (Pointer, Value, ViewStorage) -> Void,
|
set setProperty: @Sendable @escaping (Pointer, Value, ViewStorage) async -> Void,
|
||||||
pointer: Pointer.Type,
|
pointer: Pointer.Type,
|
||||||
updateStrategy: UpdateStrategy = .automatic
|
updateStrategy: UpdateStrategy = .automatic
|
||||||
) {
|
) {
|
||||||
@ -44,13 +44,13 @@ public struct Property<Value, Pointer>: PropertyProtocol {
|
|||||||
/// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases.
|
/// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases.
|
||||||
public init(
|
public init(
|
||||||
wrappedValue: Value,
|
wrappedValue: Value,
|
||||||
set setProperty: @escaping (Pointer, Value) -> Void,
|
set setProperty: @Sendable @escaping (Pointer, Value) async -> Void,
|
||||||
pointer: Pointer.Type,
|
pointer: Pointer.Type,
|
||||||
updateStrategy: UpdateStrategy = .automatic
|
updateStrategy: UpdateStrategy = .automatic
|
||||||
) {
|
) {
|
||||||
self.init(
|
self.init(
|
||||||
wrappedValue: wrappedValue,
|
wrappedValue: wrappedValue,
|
||||||
set: { pointer, value, _ in setProperty(pointer, value) },
|
set: { pointer, value, _ in await setProperty(pointer, value) },
|
||||||
pointer: pointer,
|
pointer: pointer,
|
||||||
updateStrategy: updateStrategy
|
updateStrategy: updateStrategy
|
||||||
)
|
)
|
||||||
@ -66,13 +66,13 @@ extension Property where Value: OptionalProtocol {
|
|||||||
/// - pointer: The type of the pointer.
|
/// - pointer: The type of the pointer.
|
||||||
/// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases.
|
/// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases.
|
||||||
public init(
|
public init(
|
||||||
set setProperty: @escaping (Pointer, Value.Wrapped, ViewStorage) -> Void,
|
set setProperty: @Sendable @escaping (Pointer, Value.Wrapped, ViewStorage) async -> Void,
|
||||||
pointer: Pointer.Type,
|
pointer: Pointer.Type,
|
||||||
updateStrategy: UpdateStrategy = .automatic
|
updateStrategy: UpdateStrategy = .automatic
|
||||||
) {
|
) {
|
||||||
self.setProperty = { pointer, value, storage in
|
self.setProperty = { pointer, value, storage in
|
||||||
if let value = value.optionalValue {
|
if let value = value.optionalValue {
|
||||||
setProperty(pointer, value, storage)
|
await setProperty(pointer, value, storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wrappedValue = nil
|
wrappedValue = nil
|
||||||
@ -86,12 +86,12 @@ extension Property where Value: OptionalProtocol {
|
|||||||
/// - pointer: The type of the pointer.
|
/// - pointer: The type of the pointer.
|
||||||
/// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases.
|
/// - updateStrategy: The update strategy, this should be ``UpdateStrategy/automatic`` in most cases.
|
||||||
public init(
|
public init(
|
||||||
set setProperty: @escaping (Pointer, Value.Wrapped) -> Void,
|
set setProperty: @Sendable @escaping (Pointer, Value.Wrapped) async -> Void,
|
||||||
pointer: Pointer.Type,
|
pointer: Pointer.Type,
|
||||||
updateStrategy: UpdateStrategy = .automatic
|
updateStrategy: UpdateStrategy = .automatic
|
||||||
) {
|
) {
|
||||||
self.init(
|
self.init(
|
||||||
set: { pointer, value, _ in setProperty(pointer, value) },
|
set: { pointer, value, _ in await setProperty(pointer, value) },
|
||||||
pointer: pointer,
|
pointer: pointer,
|
||||||
updateStrategy: updateStrategy
|
updateStrategy: updateStrategy
|
||||||
)
|
)
|
||||||
@ -100,7 +100,7 @@ extension Property where Value: OptionalProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The property protocol.
|
/// The property protocol.
|
||||||
protocol PropertyProtocol {
|
protocol PropertyProtocol: Sendable {
|
||||||
|
|
||||||
/// The type of the wrapped value.
|
/// The type of the wrapped value.
|
||||||
associatedtype Value
|
associatedtype Value
|
||||||
@ -110,14 +110,14 @@ protocol PropertyProtocol {
|
|||||||
/// The wrapped value.
|
/// The wrapped value.
|
||||||
var wrappedValue: Value { get }
|
var wrappedValue: Value { get }
|
||||||
/// Set the property.
|
/// Set the property.
|
||||||
var setProperty: (Pointer, Value, ViewStorage) -> Void { get }
|
var setProperty: @Sendable (Pointer, Value, ViewStorage) async -> Void { get }
|
||||||
/// The update strategy.
|
/// The update strategy.
|
||||||
var updateStrategy: UpdateStrategy { get }
|
var updateStrategy: UpdateStrategy { get }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The update strategy for properties.
|
/// The update strategy for properties.
|
||||||
public enum UpdateStrategy {
|
public enum UpdateStrategy: Sendable {
|
||||||
|
|
||||||
/// If equatable, update only when the value changed.
|
/// If equatable, update only when the value changed.
|
||||||
/// If not equatable, this is equivalent to ``UpdateStrategy/always``.
|
/// If not equatable, this is equivalent to ``UpdateStrategy/always``.
|
||||||
@ -140,9 +140,10 @@ extension Widget {
|
|||||||
public func container<Data>(
|
public func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) async -> ViewStorage where Data: ViewRenderData {
|
||||||
let storage = ViewStorage(initializeWidget())
|
let storage = ViewStorage(initializeWidget())
|
||||||
initProperties(storage, data: data, type: type)
|
await initProperties(storage, data: data, type: type)
|
||||||
|
await update(storage, data: data, updateProperties: true, type: type)
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,10 +160,10 @@ extension Widget {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
self.updateProperties(storage, data: data, updateProperties: updateProperties, type: type)
|
await self.updateProperties(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
if updateProperties {
|
if updateProperties {
|
||||||
storage.previousState = self
|
await storage.setPreviousState(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,14 +176,16 @@ extension Widget {
|
|||||||
_ storage: ViewStorage,
|
_ storage: ViewStorage,
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
let mirror = Mirror(reflecting: self)
|
let mirror = Mirror(reflecting: self)
|
||||||
for property in mirror.children {
|
for property in mirror.children {
|
||||||
if let value = property.value as? any ViewPropertyProtocol {
|
if let value = property.value as? any ViewPropertyProtocol {
|
||||||
initViewProperty(value, data: data, parent: storage, label: property.label ?? .mainContent, type: type)
|
let subview = await value.wrappedValue.storage(data: data, type: type)
|
||||||
|
await initViewProperty(value, view: subview, parent: storage)
|
||||||
|
await storage.setContent(key: property.label ?? .mainContent, value: [subview])
|
||||||
}
|
}
|
||||||
if let value = property.value as? any BindingPropertyProtocol {
|
if let value = property.value as? any BindingPropertyProtocol {
|
||||||
initBindingProperty(value, parent: storage)
|
await initBindingProperty(value, parent: storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,35 +193,29 @@ extension Widget {
|
|||||||
/// Initialize the properties wrapped with ``ViewProperty``.
|
/// Initialize the properties wrapped with ``ViewProperty``.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - value: The property.
|
/// - value: The property.
|
||||||
/// - data: The widget data.
|
/// - view: The subview's view storage.
|
||||||
/// - parent: The parent's view storage.
|
/// - parent: The parent's view storage.
|
||||||
/// - label: The view content label.
|
func initViewProperty<Property>(
|
||||||
/// - type: The view context type of the parent view.
|
|
||||||
func initViewProperty<Property, ParentContext>(
|
|
||||||
_ value: Property,
|
_ value: Property,
|
||||||
data: WidgetData,
|
view: ViewStorage,
|
||||||
parent: ViewStorage,
|
parent: ViewStorage
|
||||||
label: String,
|
) async where Property: ViewPropertyProtocol {
|
||||||
type: ParentContext.Type
|
if let view = await view.pointer as? Property.ViewPointer,
|
||||||
) where Property: ViewPropertyProtocol, ParentContext: ViewRenderData {
|
let pointer = await parent.pointer as? Property.Pointer {
|
||||||
var data = data
|
await value.setView(pointer, view)
|
||||||
if type != Property.ViewContext.self {
|
|
||||||
data = data.noModifiers
|
|
||||||
}
|
}
|
||||||
let subview = value.wrappedValue.storage(data: data, type: Property.ViewContext.self)
|
|
||||||
if let view = subview.pointer as? Property.ViewPointer, let pointer = parent.pointer as? Property.Pointer {
|
|
||||||
value.setView(pointer, view)
|
|
||||||
}
|
|
||||||
parent.content[label] = [subview]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a binding property.
|
/// Initialize a binding property.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - value: The property.
|
/// - value: The property.
|
||||||
/// - parent: The view storage.
|
/// - parent: The view storage.
|
||||||
func initBindingProperty<Property>(_ value: Property, parent: ViewStorage) where Property: BindingPropertyProtocol {
|
func initBindingProperty<Property>(
|
||||||
if let view = parent.pointer as? Property.Pointer {
|
_ value: Property,
|
||||||
value.observe(
|
parent: ViewStorage
|
||||||
|
) async where Property: BindingPropertyProtocol {
|
||||||
|
if let view = await parent.pointer as? Property.Pointer {
|
||||||
|
await value.observe(
|
||||||
view,
|
view,
|
||||||
.init {
|
.init {
|
||||||
value.wrappedValue.wrappedValue
|
value.wrappedValue.wrappedValue
|
||||||
@ -233,6 +230,150 @@ extension Widget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the properties wrapped with ``Property``.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - storage: The storage to update.
|
||||||
|
/// - data: The widget data.
|
||||||
|
/// - updateProperties: Whether to update the view's properties.
|
||||||
|
/// - type: The view render data type.
|
||||||
|
public func updateProperties<Data>(
|
||||||
|
_ storage: ViewStorage,
|
||||||
|
data: WidgetData,
|
||||||
|
updateProperties: Bool,
|
||||||
|
type: Data.Type
|
||||||
|
) async where Data: ViewRenderData {
|
||||||
|
let mirror = Mirror(reflecting: self)
|
||||||
|
await updateNotEquatable(
|
||||||
|
mirror: mirror,
|
||||||
|
storage: storage,
|
||||||
|
data: data,
|
||||||
|
updateProperties: updateProperties,
|
||||||
|
type: type
|
||||||
|
)
|
||||||
|
guard updateProperties else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await updateAlwaysWhenStateUpdate(mirror: mirror, storage: storage)
|
||||||
|
await updateEquatable(mirror: mirror, storage: storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the properties which are not equatable and should always be updated (e.g. closures).
|
||||||
|
/// - Parameters:
|
||||||
|
/// - mirror: A mirror of the widget.
|
||||||
|
/// - storage: The view storage.
|
||||||
|
/// - data: The widget data.
|
||||||
|
/// - updateProperties: Whether to update the properties.
|
||||||
|
/// - type: The view render data type.
|
||||||
|
func updateNotEquatable<Data>(
|
||||||
|
mirror: Mirror,
|
||||||
|
storage: ViewStorage,
|
||||||
|
data: WidgetData,
|
||||||
|
updateProperties: Bool,
|
||||||
|
type: Data.Type
|
||||||
|
) async where Data: ViewRenderData {
|
||||||
|
for property in mirror.children {
|
||||||
|
if let value = property.value as? any PropertyProtocol {
|
||||||
|
if value.updateStrategy == .always ||
|
||||||
|
value.wrappedValue as? any Equatable == nil && value.updateStrategy != .alwaysWhenStateUpdate {
|
||||||
|
await setProperty(property: value, storage: storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let value = property.value as? any ViewPropertyProtocol,
|
||||||
|
let storage = await storage.getContent(key: property.label ?? .mainContent).first {
|
||||||
|
await value.wrappedValue
|
||||||
|
.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
|
}
|
||||||
|
if let value = property.value as? any BindingPropertyProtocol {
|
||||||
|
await setBindingProperty(property: value, storage: storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the properties which should always be updated when a state property changed
|
||||||
|
/// (e.g. "regular" properties which are not equatable).
|
||||||
|
/// - Parameters:
|
||||||
|
/// - mirror: A mirror of the widget.
|
||||||
|
/// - storage: The view storage.
|
||||||
|
///
|
||||||
|
/// Initialize the ``Property`` property wrapper with the ``UpdateStrategy/alwaysWhenStateUpdate``.
|
||||||
|
func updateAlwaysWhenStateUpdate(mirror: Mirror, storage: ViewStorage) async {
|
||||||
|
for property in mirror.children {
|
||||||
|
if let value = property.value as? any PropertyProtocol {
|
||||||
|
if value.updateStrategy == .alwaysWhenStateUpdate {
|
||||||
|
await setProperty(property: value, storage: storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update equatable properties (most properties).
|
||||||
|
/// - Parameters:
|
||||||
|
/// - mirror: A mirror of the widget.
|
||||||
|
/// - storage: The view storage.
|
||||||
|
func updateEquatable(mirror: Mirror, storage: ViewStorage) async {
|
||||||
|
let previousState: Mirror.Children? = if let previousState = await storage.previousState {
|
||||||
|
Mirror(reflecting: previousState).children
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
for property in mirror.children {
|
||||||
|
if let value = property.value as? any PropertyProtocol,
|
||||||
|
value.updateStrategy == .automatic,
|
||||||
|
let wrappedValue = value.wrappedValue as? any Equatable {
|
||||||
|
var update = true
|
||||||
|
if let previous = previousState?.first(where: { previousProperty in
|
||||||
|
previousProperty.label == property.label
|
||||||
|
})?.value as? any PropertyProtocol,
|
||||||
|
equal(previous, wrappedValue) {
|
||||||
|
update = false
|
||||||
|
}
|
||||||
|
if update {
|
||||||
|
await setProperty(property: value, storage: storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a property is equal to a value.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - property: The property.
|
||||||
|
/// - value: The value.
|
||||||
|
/// - Returns: Whether the property and value are equal.
|
||||||
|
func equal<Property, Value>(
|
||||||
|
_ property: Property,
|
||||||
|
_ value: Value
|
||||||
|
) -> Bool where Property: PropertyProtocol, Value: Equatable {
|
||||||
|
equal(property.wrappedValue, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a value is equal to another value.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - value1: The first value.
|
||||||
|
/// - value2: The second value.
|
||||||
|
/// - Returns: Whether the values are equal.
|
||||||
|
func equal<Value1, Value2>(
|
||||||
|
_ value1: Value1,
|
||||||
|
_ value2: Value2
|
||||||
|
) -> Bool where Value2: Equatable {
|
||||||
|
if let value1 = value1 as? Value2 {
|
||||||
|
return value1 == value2
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a property to the framework.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - property: The property.
|
||||||
|
/// - storage: The view storage.
|
||||||
|
func setProperty<Property>(property: Property, storage: ViewStorage) async where Property: PropertyProtocol {
|
||||||
|
if let optional = property.wrappedValue as? any OptionalProtocol, optional.optionalValue == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let pointer = await storage.pointer as? Property.Pointer {
|
||||||
|
await property.setProperty(pointer, property.wrappedValue, storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A protocol for values which can be optional.
|
/// A protocol for values which can be optional.
|
||||||
|
|||||||
@ -10,24 +10,22 @@
|
|||||||
/// This will be used if you do not provide a custom ``Widget/update(_:data:updateProperties:type:)`` method
|
/// This will be used if you do not provide a custom ``Widget/update(_:data:updateProperties:type:)`` method
|
||||||
/// or call the ``Widget/updateProperties(_:updateProperties:)`` method in your custom update method.
|
/// or call the ``Widget/updateProperties(_:updateProperties:)`` method in your custom update method.
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
public struct ViewProperty<Pointer, ViewPointer, ViewContext>: ViewPropertyProtocol where ViewContext: ViewRenderData {
|
public struct ViewProperty<Pointer, ViewPointer>: ViewPropertyProtocol {
|
||||||
|
|
||||||
/// The wrapped value.
|
/// The wrapped value.
|
||||||
public var wrappedValue: Body = []
|
public var wrappedValue: Body = []
|
||||||
/// Set the view.
|
/// Set the view.
|
||||||
var setView: (Pointer, ViewPointer) -> Void
|
var setView: @Sendable (Pointer, ViewPointer) async -> Void
|
||||||
|
|
||||||
/// Initialize a property.
|
/// Initialize a property.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - setView: Set the view.
|
/// - setView: Set the view.
|
||||||
/// - pointer: The pointer type of the parent view (usually a concrete view type).
|
/// - pointer: The pointer type of the parent view (usually a concrete view type).
|
||||||
/// - subview: The pointer type of the child view (usually a protocol, view class, or similar).
|
/// - subview: The pointer type of the child view (usually a protocol, view class, or similar).
|
||||||
/// - context: The view render data type.
|
|
||||||
public init(
|
public init(
|
||||||
set setView: @escaping (Pointer, ViewPointer) -> Void,
|
set setView: @Sendable @escaping (Pointer, ViewPointer) async -> Void,
|
||||||
pointer: Pointer.Type,
|
pointer: Pointer.Type,
|
||||||
subview: ViewPointer.Type,
|
subview: ViewPointer.Type
|
||||||
context: ViewContext.Type
|
|
||||||
) {
|
) {
|
||||||
self.setView = setView
|
self.setView = setView
|
||||||
}
|
}
|
||||||
@ -37,18 +35,16 @@ public struct ViewProperty<Pointer, ViewPointer, ViewContext>: ViewPropertyProto
|
|||||||
/// The view property protocol.
|
/// The view property protocol.
|
||||||
///
|
///
|
||||||
/// Do not use for wrapper widgets.
|
/// Do not use for wrapper widgets.
|
||||||
protocol ViewPropertyProtocol {
|
protocol ViewPropertyProtocol: Sendable {
|
||||||
|
|
||||||
/// The type of the view's pointer.
|
/// The type of the view's pointer.
|
||||||
associatedtype Pointer
|
associatedtype Pointer
|
||||||
/// The type of the view's content.
|
/// The type of the view's content.
|
||||||
associatedtype ViewPointer
|
associatedtype ViewPointer
|
||||||
/// The view render data type.
|
|
||||||
associatedtype ViewContext: ViewRenderData
|
|
||||||
|
|
||||||
/// The wrapped value.
|
/// The wrapped value.
|
||||||
var wrappedValue: Body { get }
|
var wrappedValue: Body { get }
|
||||||
/// Set the view.
|
/// Set the view.
|
||||||
var setView: (Pointer, ViewPointer) -> Void { get }
|
var setView: @Sendable (Pointer, ViewPointer) async -> Void { get }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,175 +0,0 @@
|
|||||||
//
|
|
||||||
// Property+.swift
|
|
||||||
// Meta
|
|
||||||
//
|
|
||||||
// Created by david-swift on 14.10.24.
|
|
||||||
//
|
|
||||||
|
|
||||||
extension Widget {
|
|
||||||
|
|
||||||
/// Update the properties wrapped with ``Property``.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - storage: The storage to update.
|
|
||||||
/// - data: The widget data.
|
|
||||||
/// - updateProperties: Whether to update the view's properties.
|
|
||||||
/// - type: The view render data type.
|
|
||||||
public func updateProperties<Data>(
|
|
||||||
_ storage: ViewStorage,
|
|
||||||
data: WidgetData,
|
|
||||||
updateProperties: Bool,
|
|
||||||
type: Data.Type
|
|
||||||
) where Data: ViewRenderData {
|
|
||||||
let mirror = Mirror(reflecting: self)
|
|
||||||
updateNotEquatable(mirror: mirror, storage: storage, data: data, updateProperties: updateProperties, type: type)
|
|
||||||
guard updateProperties else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateAlwaysWhenStateUpdate(mirror: mirror, storage: storage)
|
|
||||||
updateEquatable(mirror: mirror, storage: storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the properties which are not equatable and should always be updated (e.g. closures).
|
|
||||||
/// - Parameters:
|
|
||||||
/// - mirror: A mirror of the widget.
|
|
||||||
/// - storage: The view storage.
|
|
||||||
/// - data: The widget data.
|
|
||||||
/// - updateProperties: Whether to update the properties.
|
|
||||||
/// - type: The view render data type.
|
|
||||||
func updateNotEquatable<Data>(
|
|
||||||
mirror: Mirror,
|
|
||||||
storage: ViewStorage,
|
|
||||||
data: WidgetData,
|
|
||||||
updateProperties: Bool,
|
|
||||||
type: Data.Type
|
|
||||||
) where Data: ViewRenderData {
|
|
||||||
for property in mirror.children {
|
|
||||||
if let value = property.value as? any PropertyProtocol {
|
|
||||||
if value.updateStrategy == .always ||
|
|
||||||
value.wrappedValue as? any Equatable == nil && value.updateStrategy != .alwaysWhenStateUpdate {
|
|
||||||
setProperty(property: value, storage: storage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let value = property.value as? any ViewPropertyProtocol,
|
|
||||||
let storage = storage.content[property.label ?? .mainContent]?.first {
|
|
||||||
updateViewProperty(
|
|
||||||
value: value,
|
|
||||||
storage: storage,
|
|
||||||
data: data,
|
|
||||||
updateProperties: updateProperties,
|
|
||||||
type: type
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if let value = property.value as? any BindingPropertyProtocol {
|
|
||||||
setBindingProperty(property: value, storage: storage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update a view property.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - value: The property.
|
|
||||||
/// - storage: The view storage.
|
|
||||||
/// - data: The widget data.
|
|
||||||
/// - updateProperties: Whether to update the properties.
|
|
||||||
/// - type: The parent context type.
|
|
||||||
func updateViewProperty<Property, ParentContext>(
|
|
||||||
value: Property,
|
|
||||||
storage: ViewStorage,
|
|
||||||
data: WidgetData,
|
|
||||||
updateProperties: Bool,
|
|
||||||
type: ParentContext.Type
|
|
||||||
) where Property: ViewPropertyProtocol, ParentContext: ViewRenderData {
|
|
||||||
var data = data
|
|
||||||
if type != Property.ViewContext.self {
|
|
||||||
data = data.noModifiers
|
|
||||||
}
|
|
||||||
value.wrappedValue
|
|
||||||
.updateStorage(storage, data: data, updateProperties: updateProperties, type: Property.ViewContext.self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the properties which should always be updated when a state property changed
|
|
||||||
/// (e.g. "regular" properties which are not equatable).
|
|
||||||
/// - Parameters:
|
|
||||||
/// - mirror: A mirror of the widget.
|
|
||||||
/// - storage: The view storage.
|
|
||||||
///
|
|
||||||
/// Initialize the ``Property`` property wrapper with the ``UpdateStrategy/alwaysWhenStateUpdate``.
|
|
||||||
func updateAlwaysWhenStateUpdate(mirror: Mirror, storage: ViewStorage) {
|
|
||||||
for property in mirror.children {
|
|
||||||
if let value = property.value as? any PropertyProtocol {
|
|
||||||
if value.updateStrategy == .alwaysWhenStateUpdate {
|
|
||||||
setProperty(property: value, storage: storage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update equatable properties (most properties).
|
|
||||||
/// - Parameters:
|
|
||||||
/// - mirror: A mirror of the widget.
|
|
||||||
/// - storage: The view storage.
|
|
||||||
func updateEquatable(mirror: Mirror, storage: ViewStorage) {
|
|
||||||
let previousState: Mirror.Children? = if let previousState = storage.previousState {
|
|
||||||
Mirror(reflecting: previousState).children
|
|
||||||
} else {
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
for property in mirror.children {
|
|
||||||
if let value = property.value as? any PropertyProtocol,
|
|
||||||
value.updateStrategy == .automatic,
|
|
||||||
let wrappedValue = value.wrappedValue as? any Equatable {
|
|
||||||
var update = true
|
|
||||||
if let previous = previousState?.first(where: { previousProperty in
|
|
||||||
previousProperty.label == property.label
|
|
||||||
})?.value as? any PropertyProtocol,
|
|
||||||
equal(previous, wrappedValue) {
|
|
||||||
update = false
|
|
||||||
}
|
|
||||||
if update {
|
|
||||||
setProperty(property: value, storage: storage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether a property is equal to a value.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - property: The property.
|
|
||||||
/// - value: The value.
|
|
||||||
/// - Returns: Whether the property and value are equal.
|
|
||||||
func equal<Property, Value>(
|
|
||||||
_ property: Property,
|
|
||||||
_ value: Value
|
|
||||||
) -> Bool where Property: PropertyProtocol, Value: Equatable {
|
|
||||||
equal(property.wrappedValue, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether a value is equal to another value.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - value1: The first value.
|
|
||||||
/// - value2: The second value.
|
|
||||||
/// - Returns: Whether the values are equal.
|
|
||||||
func equal<Value1, Value2>(
|
|
||||||
_ value1: Value1,
|
|
||||||
_ value2: Value2
|
|
||||||
) -> Bool where Value2: Equatable {
|
|
||||||
if let value1 = value1 as? Value2 {
|
|
||||||
return value1 == value2
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply a property to the framework.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - property: The property.
|
|
||||||
/// - storage: The view storage.
|
|
||||||
func setProperty<Property>(property: Property, storage: ViewStorage) where Property: PropertyProtocol {
|
|
||||||
if let optional = property.wrappedValue as? any OptionalProtocol, optional.optionalValue == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let pointer = storage.pointer as? Property.Pointer {
|
|
||||||
property.setProperty(pointer, property.wrappedValue, 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(), environment: getEnvironmentVariables())]
|
[StateWrapper(content: { view }, state: getState())]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the state from the properties.
|
/// Get the state from the properties.
|
||||||
@ -47,16 +47,4 @@ 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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,18 +6,18 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// Store a reference to a rendered view in a view storage.
|
/// Store a reference to a rendered view in a view storage.
|
||||||
public class ViewStorage {
|
public actor ViewStorage: Sendable {
|
||||||
|
|
||||||
/// The pointer.
|
/// The pointer.
|
||||||
///
|
///
|
||||||
/// It can be a C pointer, a Swift class, or other information depending on the backend.
|
/// It can be a C pointer, a Swift class, or other information depending on the backend.
|
||||||
public var pointer: Any?
|
public var pointer: Sendable?
|
||||||
/// The view's content for container widgets.
|
/// The view's content for container widgets.
|
||||||
public var content: [String: [ViewStorage]]
|
var content: [String: [ViewStorage]]
|
||||||
/// The view's state (used in `StateWrapper`).
|
/// The view's state (used in `StateWrapper`).
|
||||||
var state: [String: StateProtocol] = [:]
|
var state: [String: StateProtocol] = [:]
|
||||||
/// Various properties of a widget.
|
/// Various properties of a widget.
|
||||||
public var fields: [String: Any] = [:]
|
var fields: [String: Sendable] = [:]
|
||||||
/// The previous state of the widget.
|
/// The previous state of the widget.
|
||||||
public var previousState: Widget?
|
public var previousState: Widget?
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ public class ViewStorage {
|
|||||||
/// - pointer: The pointer to the widget, its type depends on the backend.
|
/// - pointer: The pointer to the widget, its type depends on the backend.
|
||||||
/// - content: The view's content for container widgets.
|
/// - content: The view's content for container widgets.
|
||||||
public init(
|
public init(
|
||||||
_ pointer: Any?,
|
_ pointer: Sendable?,
|
||||||
content: [String: [ViewStorage]] = [:],
|
content: [String: [ViewStorage]] = [:],
|
||||||
state: Widget? = nil
|
state: Widget? = nil
|
||||||
) {
|
) {
|
||||||
@ -45,4 +45,88 @@ public class ViewStorage {
|
|||||||
self.previousState = state
|
self.previousState = state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize a view storage.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - pointer: The opaque pointer.
|
||||||
|
/// - content: The view's content for container widgets.
|
||||||
|
public init(
|
||||||
|
_ pointer: OpaquePointer?,
|
||||||
|
content: [String: [ViewStorage]] = [:],
|
||||||
|
state: Widget? = nil
|
||||||
|
) {
|
||||||
|
self.pointer = pointer
|
||||||
|
self.content = content
|
||||||
|
self.previousState = state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the state under a certain key.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key.
|
||||||
|
/// - value: The state.
|
||||||
|
func setState(key: String, value: StateProtocol) {
|
||||||
|
state[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the state.
|
||||||
|
/// - Parameter state: The state.
|
||||||
|
func setState(_ state: [String: StateProtocol]) {
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the state under a certain key.
|
||||||
|
/// - Parameter key: The key.
|
||||||
|
/// - Returns: The state.
|
||||||
|
func getState(key: String) -> StateProtocol? {
|
||||||
|
state[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the pointer.
|
||||||
|
/// - Parameter value: The new pointer.
|
||||||
|
public func setPointer(_ value: Sendable?) {
|
||||||
|
pointer = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the element of a certain field.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key.
|
||||||
|
/// - value: The field.
|
||||||
|
public func setField(key: String, value: Sendable) {
|
||||||
|
fields[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a certain field.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key.
|
||||||
|
public func removeField(key: String) {
|
||||||
|
fields.removeValue(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the element of a certain field.
|
||||||
|
/// - Parameter key: The key.
|
||||||
|
/// - Returns: The field.
|
||||||
|
public func getField(key: String) -> Sendable? {
|
||||||
|
fields[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the content elements under a certain key.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - key: The key.
|
||||||
|
/// - value: The content elements.
|
||||||
|
public func setContent(key: String, value: [ViewStorage]) {
|
||||||
|
content[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the content elements under a certain key.
|
||||||
|
/// - Parameter key: The key.
|
||||||
|
/// - Returns: The content elements.
|
||||||
|
public func getContent(key: String) -> [ViewStorage] {
|
||||||
|
content[key] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the previous state.
|
||||||
|
/// - Parameter state: The state.
|
||||||
|
public func setPreviousState(_ state: Widget?) {
|
||||||
|
previousState = state
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ public protocol Widget: AnyView {
|
|||||||
func container<Data>(
|
func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData
|
) async -> ViewStorage where Data: ViewRenderData
|
||||||
|
|
||||||
/// Update the stored content.
|
/// Update the stored content.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@ -32,14 +32,14 @@ public protocol Widget: AnyView {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData
|
) async where Data: ViewRenderData
|
||||||
|
|
||||||
/// Get the widget.
|
/// Get the widget.
|
||||||
/// - Returns: The widget.
|
/// - Returns: The widget.
|
||||||
///
|
///
|
||||||
/// Define this function only if you do not define ``Widget/container(data:type:)``.
|
/// Define this function only if you do not define ``Widget/container(data:type:)``.
|
||||||
/// Otherwise, it will not have an effect.
|
/// Otherwise, it will not have an effect.
|
||||||
func initializeWidget() -> Any
|
func initializeWidget() -> Sendable
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ extension Widget {
|
|||||||
|
|
||||||
/// Print a warning if the widget does not set this function but it gets accessed.
|
/// Print a warning if the widget does not set this function but it gets accessed.
|
||||||
/// - Returns: A dummy pointer.
|
/// - Returns: A dummy pointer.
|
||||||
public func initializeWidget() -> Any {
|
public func initializeWidget() -> Sendable {
|
||||||
print("Warning: Define initialize widget function or container function for \(Self.self)")
|
print("Warning: Define initialize widget function or container function for \(Self.self)")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,16 +6,16 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
/// Data passed to widgets when initializing or updating the container.
|
/// Data passed to widgets when initializing or updating the container.
|
||||||
public struct WidgetData {
|
public struct WidgetData: Sendable {
|
||||||
|
|
||||||
/// The view modifiers.
|
/// The view modifiers.
|
||||||
public var modifiers: [(AnyView) -> AnyView] = []
|
public var modifiers: [@Sendable (AnyView) -> AnyView] = []
|
||||||
/// The scene storage of the parent scene element.
|
/// The scene storage of the parent scene element.
|
||||||
public var sceneStorage: SceneStorage
|
public var sceneStorage: SceneStorage
|
||||||
/// The app storage of the parent app.
|
/// The app storage of the parent app.
|
||||||
public var appStorage: any AppStorage
|
public var appStorage: any AppStorage
|
||||||
/// Fields for custom data.
|
/// Fields for custom data.
|
||||||
public var fields: [String: Any] = [:]
|
public var fields: [String: Sendable] = [:]
|
||||||
|
|
||||||
/// Modify the data so that there are no modifiers.
|
/// Modify the data so that there are no modifiers.
|
||||||
public var noModifiers: Self {
|
public var noModifiers: Self {
|
||||||
|
|||||||
@ -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, WidgetData) -> Void
|
var modify: @Sendable (ViewStorage) async -> Void
|
||||||
/// The wrapped view.
|
/// The wrapped view.
|
||||||
var content: AnyView
|
var content: AnyView
|
||||||
|
|
||||||
@ -21,9 +21,9 @@ struct AppearObserver: ConvenienceWidget {
|
|||||||
func container<Data>(
|
func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) async -> ViewStorage where Data: ViewRenderData {
|
||||||
let storage = content.storage(data: data, type: type)
|
let storage = await content.storage(data: data, type: type)
|
||||||
modify(storage, data)
|
await modify(storage)
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,8 +38,8 @@ struct AppearObserver: ConvenienceWidget {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
await content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -50,22 +50,15 @@ 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, WidgetData) -> Void) -> AnyView {
|
public func inspectOnAppear(_ closure: @Sendable @escaping (ViewStorage) async -> 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.
|
||||||
public func onAppear(_ closure: @escaping () -> Void) -> AnyView {
|
public func onAppear(_ closure: @Sendable @escaping () async -> Void) -> AnyView {
|
||||||
inspectOnAppear { _ in closure() }
|
inspectOnAppear { _ in await closure() }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ struct ContentModifier<Content>: ConvenienceWidget where Content: AnyView {
|
|||||||
/// The wrapped view.
|
/// The wrapped view.
|
||||||
var content: AnyView
|
var content: AnyView
|
||||||
/// The closure for the modification.
|
/// The closure for the modification.
|
||||||
var modify: (Content) -> AnyView
|
var modify: @Sendable (Content) -> AnyView
|
||||||
|
|
||||||
/// The view storage.
|
/// The view storage.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@ -21,8 +21,8 @@ struct ContentModifier<Content>: ConvenienceWidget where Content: AnyView {
|
|||||||
func container<Data>(
|
func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) async -> ViewStorage where Data: ViewRenderData {
|
||||||
content.storage(data: data.modify { $0.modifiers += [modifyView] }, type: type)
|
await content.storage(data: data.modify { $0.modifiers += [modifyView] }, type: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the stored content.
|
/// Update the stored content.
|
||||||
@ -36,8 +36,8 @@ struct ContentModifier<Content>: ConvenienceWidget where Content: AnyView {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
content
|
await content
|
||||||
.updateStorage(
|
.updateStorage(
|
||||||
storage,
|
storage,
|
||||||
data: data.modify { $0.modifiers += [modifyView] },
|
data: data.modify { $0.modifiers += [modifyView] },
|
||||||
@ -68,7 +68,7 @@ extension AnyView {
|
|||||||
/// - Returns: A view.
|
/// - Returns: A view.
|
||||||
public func modifyContent<Content>(
|
public func modifyContent<Content>(
|
||||||
_ type: Content.Type,
|
_ type: Content.Type,
|
||||||
modify: @escaping (Content) -> AnyView
|
modify: @Sendable @escaping (Content) -> AnyView
|
||||||
) -> AnyView where Content: AnyView {
|
) -> AnyView where Content: AnyView {
|
||||||
ContentModifier(content: self, modify: modify)
|
ContentModifier(content: self, modify: modify)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -23,9 +23,9 @@ struct DummyEitherView: ConvenienceWidget {
|
|||||||
func container<Data>(
|
func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) async -> ViewStorage where Data: ViewRenderData {
|
||||||
let content = type.EitherViewType(condition) { view1 ?? [] } else: { view2 ?? [] }
|
let content = type.EitherViewType(condition) { view1 ?? [] } else: { view2 ?? [] }
|
||||||
let storage = content.storage(data: data, type: type)
|
let storage = await content.storage(data: data, type: type)
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +40,9 @@ struct DummyEitherView: ConvenienceWidget {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
let content = type.EitherViewType(condition) { view1 ?? [] } else: { view2 ?? [] }
|
let content = type.EitherViewType(condition) { view1 ?? [] } else: { view2 ?? [] }
|
||||||
content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
await content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,8 +21,8 @@ struct Freeze: ConvenienceWidget {
|
|||||||
func container<Data>(
|
func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) async -> ViewStorage where Data: ViewRenderData {
|
||||||
content.storage(data: data, type: type)
|
await content.storage(data: data, type: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the stored content.
|
/// Update the stored content.
|
||||||
@ -36,11 +36,11 @@ struct Freeze: ConvenienceWidget {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
guard !freeze else {
|
guard !freeze else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
await content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, WidgetData, Bool) -> Void
|
var modify: @Sendable (ViewStorage, Bool) async -> Void
|
||||||
/// The wrapped view.
|
/// The wrapped view.
|
||||||
var content: AnyView
|
var content: AnyView
|
||||||
|
|
||||||
@ -21,8 +21,10 @@ struct InspectorWrapper: ConvenienceWidget {
|
|||||||
func container<Data>(
|
func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) async -> ViewStorage where Data: ViewRenderData {
|
||||||
content.storage(data: data, type: type)
|
let storage = await content.storage(data: data, type: type)
|
||||||
|
await modify(storage, true)
|
||||||
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the stored content.
|
/// Update the stored content.
|
||||||
@ -36,9 +38,9 @@ struct InspectorWrapper: ConvenienceWidget {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
await content.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
modify(storage, data, updateProperties)
|
await modify(storage, updateProperties)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -49,22 +51,15 @@ 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, WidgetData, Bool) -> Void) -> AnyView {
|
public func inspect(_ modify: @Sendable @escaping (ViewStorage, Bool) async -> 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.
|
||||||
public func onUpdate(_ onUpdate: @escaping () -> Void) -> AnyView {
|
public func onUpdate(_ onUpdate: @Sendable @escaping () async -> Void) -> AnyView {
|
||||||
inspect { _, _ in onUpdate() }
|
inspect { _, _ in await onUpdate() }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,8 @@ struct ModifierStopper: ConvenienceWidget {
|
|||||||
func container<Data>(
|
func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) async -> ViewStorage where Data: ViewRenderData {
|
||||||
content.storage(data: data.noModifiers, type: type)
|
await content.storage(data: data.noModifiers, type: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the stored content.
|
/// Update the stored content.
|
||||||
@ -34,8 +34,8 @@ struct ModifierStopper: ConvenienceWidget {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
content.updateStorage(storage, data: data.noModifiers, updateProperties: updateProperties, type: type)
|
await content.updateStorage(storage, data: data.noModifiers, updateProperties: updateProperties, type: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
//
|
|
||||||
// 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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -9,15 +9,13 @@
|
|||||||
struct StateWrapper: ConvenienceWidget {
|
struct StateWrapper: ConvenienceWidget {
|
||||||
|
|
||||||
/// The content.
|
/// The content.
|
||||||
var content: () -> Body
|
var content: @Sendable () -> 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.
|
||||||
init(@ViewBuilder content: @escaping () -> Body) {
|
init(@ViewBuilder content: @Sendable @escaping () -> Body) {
|
||||||
self.content = content
|
self.content = content
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,15 +23,9 @@ struct StateWrapper: ConvenienceWidget {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - content: The view content.
|
/// - content: The view content.
|
||||||
/// - state: The state information.
|
/// - state: The state information.
|
||||||
/// - environment: The environment properties.
|
init(content: @Sendable @escaping () -> Body, state: [String: StateProtocol]) {
|
||||||
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.
|
||||||
@ -48,10 +40,10 @@ struct StateWrapper: ConvenienceWidget {
|
|||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
updateProperties: Bool,
|
updateProperties: Bool,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) where Data: ViewRenderData {
|
) async where Data: ViewRenderData {
|
||||||
var updateProperties = updateProperties
|
var updateProperties = updateProperties
|
||||||
for property in state {
|
for property in state {
|
||||||
if let storage = storage.state[property.key]?.content.storage {
|
if let storage = await storage.getState(key: property.key)?.content.storage {
|
||||||
property.value.content.storage = storage
|
property.value.content.storage = storage
|
||||||
}
|
}
|
||||||
if property.value.content.update {
|
if property.value.content.update {
|
||||||
@ -59,11 +51,16 @@ struct StateWrapper: ConvenienceWidget {
|
|||||||
property.value.content.update = false
|
property.value.content.update = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assignEnvironment(data: data)
|
guard let storage = await storage.getContent(key: .mainContent).first else {
|
||||||
guard let storage = storage.content[.mainContent]?.first else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content().updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
await content().updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
|
||||||
|
for property in state {
|
||||||
|
if var value = property.value.content.value as? Signal, value.update {
|
||||||
|
value.destroySignal()
|
||||||
|
property.value.content.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a view storage.
|
/// Get a view storage.
|
||||||
@ -74,23 +71,14 @@ struct StateWrapper: ConvenienceWidget {
|
|||||||
func container<Data>(
|
func container<Data>(
|
||||||
data: WidgetData,
|
data: WidgetData,
|
||||||
type: Data.Type
|
type: Data.Type
|
||||||
) -> ViewStorage where Data: ViewRenderData {
|
) async -> ViewStorage where Data: ViewRenderData {
|
||||||
assignEnvironment(data: data)
|
let content = await content().storage(data: data, type: type)
|
||||||
let content = content().storage(data: data, type: type)
|
let storage = ViewStorage(await content.pointer, content: [.mainContent: [content]])
|
||||||
let storage = ViewStorage(content.pointer, content: [.mainContent: [content]])
|
await storage.setState(state)
|
||||||
storage.state = state
|
|
||||||
for element in state {
|
for element in state {
|
||||||
element.value.setup()
|
element.value.setup()
|
||||||
}
|
}
|
||||||
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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
add_subdirectory(SampleBackends)
|
|
||||||
add_subdirectory(DemoApp)
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
@testable import Meta
|
import Meta
|
||||||
import SampleBackends
|
import SampleBackends
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@ -14,16 +14,16 @@ struct TestExecutable {
|
|||||||
|
|
||||||
struct DemoApp: App {
|
struct DemoApp: App {
|
||||||
|
|
||||||
|
let id = "io.github.AparokshaUI.DemoApp"
|
||||||
// #if os(...)
|
// #if os(...)
|
||||||
var app = Backend1.Backend1App()
|
var app: Backend1.Backend1App!
|
||||||
// #else
|
// #else
|
||||||
// var app = Backend2.Backend2App()
|
// var app: Backend2.Backend2App!
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,8 +32,6 @@ 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
|
||||||
|
|
||||||
@ -49,7 +47,6 @@ struct DemoView: View {
|
|||||||
app.addSceneElement("main")
|
app.addSceneElement("main")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear { print(test ?? 0) }
|
|
||||||
}
|
}
|
||||||
TestView()
|
TestView()
|
||||||
testContent
|
testContent
|
||||||
@ -76,7 +73,7 @@ struct TestView: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestModel: Model {
|
struct TestModel: Sendable, Model {
|
||||||
|
|
||||||
var test = "Label"
|
var test = "Label"
|
||||||
|
|
||||||
|
|||||||
@ -6,16 +6,16 @@ public enum Backend1 {
|
|||||||
|
|
||||||
public init() { }
|
public init() { }
|
||||||
|
|
||||||
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
|
public func container<Data>(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData {
|
||||||
print("Init test widget 1")
|
print("Init test widget 1")
|
||||||
let storage = ViewStorage(nil)
|
let storage = ViewStorage(nil)
|
||||||
storage.fields["test"] = 0
|
await storage.setField(key: "test", value: 0)
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update<Data>(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) {
|
public func update<Data>(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async {
|
||||||
print("Update test widget 1 (#\(storage.fields["test"] ?? ""))")
|
print("Update test widget 1 (#\(await storage.getField(key: "test") ?? ""))")
|
||||||
storage.fields["test"] = (storage.fields["test"] as? Int ?? 0) + 1
|
await storage.setField(key: "test", value: (storage.getField(key: "test") as? Int ?? 0) + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -24,16 +24,16 @@ public enum Backend1 {
|
|||||||
|
|
||||||
public init() { }
|
public init() { }
|
||||||
|
|
||||||
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
|
public func container<Data>(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData {
|
||||||
print("Init test widget 3")
|
print("Init test widget 3")
|
||||||
let storage = ViewStorage(nil)
|
let storage = ViewStorage(nil)
|
||||||
storage.fields["test"] = 0
|
await storage.setField(key: "test", value: 0)
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update<Data>(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) {
|
public func update<Data>(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async {
|
||||||
print("Update test widget 3 (#\(storage.fields["test"] ?? ""))")
|
print("Update test widget 3 (#\(await storage.getField(key: "test") ?? ""))")
|
||||||
storage.fields["test"] = (storage.fields["test"] as? Int ?? 0) + 1
|
await storage.setField(key: "test", value: (storage.getField(key: "test") as? Int ?? 0) + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -42,28 +42,28 @@ public enum Backend1 {
|
|||||||
|
|
||||||
@Property(set: { print("Update button (label = \($1))") }, pointer: Any.self)
|
@Property(set: { print("Update button (label = \($1))") }, pointer: Any.self)
|
||||||
var label = ""
|
var label = ""
|
||||||
@Property(set: { $2.fields["action"] = $1 }, pointer: Any.self)
|
@Property(set: { $2.setField(key: "action", value: $1) }, pointer: Any.self)
|
||||||
var action: () -> Void = { }
|
var action: @Sendable () -> Void = { }
|
||||||
|
|
||||||
public init(_ label: String, action: @escaping () -> Void) {
|
public init(_ label: String, action: @Sendable @escaping () -> Void) {
|
||||||
self.label = label
|
self.label = label
|
||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
|
public func container<Data>(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData {
|
||||||
print("Init button")
|
print("Init button")
|
||||||
let storage = ViewStorage(nil)
|
let storage = ViewStorage(nil)
|
||||||
Task {
|
Task {
|
||||||
try await Task.sleep(nanoseconds: 1_000_000_000)
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
(storage.fields["action"] as? () -> Void)?()
|
(await storage.getField(key: "action") as? @Sendable () -> Void)?()
|
||||||
}
|
}
|
||||||
storage.fields["action"] = action
|
await storage.setField(key: "action", value: action)
|
||||||
storage.previousState = self
|
await storage.setPreviousState(self)
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Window: BackendSceneElement {
|
public struct Window: BackendSceneElement, Sendable {
|
||||||
|
|
||||||
public var id: String
|
public var id: String
|
||||||
var spawn: Int
|
var spawn: Int
|
||||||
@ -77,7 +77,9 @@ public enum Backend1 {
|
|||||||
|
|
||||||
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
|
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
|
||||||
for _ in 0..<spawn {
|
for _ in 0..<spawn {
|
||||||
app.storage.sceneStorage.append(container(app: app))
|
Task {
|
||||||
|
await app.appendScene(container(app: app))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,17 +88,21 @@ public enum Backend1 {
|
|||||||
let storage = SceneStorage(id: id, pointer: nil) {
|
let storage = SceneStorage(id: id, pointer: nil) {
|
||||||
print("Make visible")
|
print("Make visible")
|
||||||
}
|
}
|
||||||
let viewStorage = content.storage(data: .init(sceneStorage: storage, appStorage: app), type: MainViewRenderData.self)
|
Task {
|
||||||
storage.content[.mainContent] = [viewStorage]
|
let viewStorage = await content.storage(data: .init(sceneStorage: storage, appStorage: app), type: MainViewRenderData.self)
|
||||||
|
await storage.setContent(key: .mainContent, value: [viewStorage])
|
||||||
|
}
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update<Storage>(_ storage: SceneStorage, app: Storage, updateProperties: Bool) where Storage: AppStorage {
|
public func update<Storage>(_ storage: SceneStorage, app: Storage, updateProperties: Bool) where Storage: AppStorage {
|
||||||
|
Task {
|
||||||
print("Update \(id)")
|
print("Update \(id)")
|
||||||
guard let viewStorage = storage.content[.mainContent]?.first else {
|
guard let viewStorage = await storage.getContent(key: .mainContent).first else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content.updateStorage(viewStorage, data: .init(sceneStorage: storage, appStorage: app), updateProperties: updateProperties, type: MainViewRenderData.self)
|
await content.updateStorage(viewStorage, data: .init(sceneStorage: storage, appStorage: app), updateProperties: updateProperties, type: MainViewRenderData.self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -109,17 +115,15 @@ public enum Backend1 {
|
|||||||
self.content = content()
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func container<Data>(data: WidgetData, type: Data.Type) -> Meta.ViewStorage where Data: ViewRenderData {
|
public func container<Data>(data: WidgetData, type: Data.Type) async -> Meta.ViewStorage where Data: ViewRenderData {
|
||||||
let storage = ViewStorage(nil)
|
let storage = ViewStorage(nil)
|
||||||
storage.content = [.mainContent: content.storages(data: data, type: type)]
|
await storage.setContent(key: .mainContent, value: content.storages(data: data, type: type))
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update<Data>(_ storage: Meta.ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData {
|
public func update<Data>(_ storage: Meta.ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async where Data: ViewRenderData {
|
||||||
guard let storages = storage.content[.mainContent] else {
|
let storages = await storage.getContent(key: .mainContent)
|
||||||
return
|
await content.update(storages, data: data, updateProperties: updateProperties, type: type)
|
||||||
}
|
|
||||||
content.update(storages, data: data, updateProperties: updateProperties, type: type)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -150,19 +154,19 @@ public enum Backend1 {
|
|||||||
|
|
||||||
public protocol BackendSceneElement: SceneElement { }
|
public protocol BackendSceneElement: SceneElement { }
|
||||||
|
|
||||||
public class Backend1App: AppStorage {
|
public actor Backend1App: AppStorage {
|
||||||
|
|
||||||
public typealias SceneElementType = BackendSceneElement
|
public typealias SceneElementType = BackendSceneElement
|
||||||
|
|
||||||
public var storage: StandardAppStorage = .init()
|
public var storage: StandardAppStorage = .init()
|
||||||
|
|
||||||
public init() { }
|
public init(id: String) { }
|
||||||
|
|
||||||
public func run(setup: @escaping () -> Void) {
|
nonisolated public func run(setup: @escaping () -> Void) {
|
||||||
setup()
|
setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func quit() {
|
nonisolated public func quit() {
|
||||||
fatalError("Quit")
|
fatalError("Quit")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,16 +6,16 @@ public enum Backend2 {
|
|||||||
|
|
||||||
public init() { }
|
public init() { }
|
||||||
|
|
||||||
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
|
public func container<Data>(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData {
|
||||||
print("Init test widget 2")
|
print("Init test widget 2")
|
||||||
let storage = ViewStorage(nil)
|
let storage = ViewStorage(nil)
|
||||||
storage.fields["test"] = 0
|
await storage.setField(key: "test", value: 0)
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update<Data>(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) {
|
public func update<Data>(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async {
|
||||||
print("Update test widget 2 (#\(storage.fields["test"] ?? ""))")
|
print("Update test widget 2 (#\(await storage.getField(key: "test") ?? ""))")
|
||||||
storage.fields["test"] = (storage.fields["test"] as? Int ?? 0) + 1
|
await storage.setField(key: "test", value: (storage.getField(key: "test") as? Int ?? 0) + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -24,16 +24,16 @@ public enum Backend2 {
|
|||||||
|
|
||||||
public init() { }
|
public init() { }
|
||||||
|
|
||||||
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
|
public func container<Data>(data: WidgetData, type: Data.Type) async -> ViewStorage where Data: ViewRenderData {
|
||||||
print("Init test widget 4")
|
print("Init test widget 4")
|
||||||
let storage = ViewStorage(nil)
|
let storage = ViewStorage(nil)
|
||||||
storage.fields["test"] = 0
|
await storage.setField(key: "test", value: 0)
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update<Data>(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) {
|
public func update<Data>(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async {
|
||||||
print("Update test widget 4 (#\(storage.fields["test"] ?? ""))")
|
print("Update test widget 4 (#\(await storage.getField(key: "test") ?? ""))")
|
||||||
storage.fields["test"] = (storage.fields["test"] as? Int ?? 0) + 1
|
await storage.setField(key: "test", value: (storage.getField(key: "test") as? Int ?? 0) + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -46,17 +46,15 @@ public enum Backend2 {
|
|||||||
self.content = content()
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func container<Data>(data: WidgetData, type: Data.Type) -> Meta.ViewStorage where Data: ViewRenderData {
|
public func container<Data>(data: WidgetData, type: Data.Type) async -> Meta.ViewStorage where Data: ViewRenderData {
|
||||||
let storage = ViewStorage(nil)
|
let storage = ViewStorage(nil)
|
||||||
storage.content = [.mainContent: content.storages(data: data, type: type)]
|
await storage.setContent(key: .mainContent, value: content.storages(data: data, type: type))
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update<Data>(_ storage: Meta.ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData {
|
public func update<Data>(_ storage: Meta.ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) async where Data: ViewRenderData {
|
||||||
guard let storages = storage.content[.mainContent] else {
|
let storages = await storage.getContent(key: .mainContent)
|
||||||
return
|
await content.update(storages, data: data, updateProperties: updateProperties, type: type)
|
||||||
}
|
|
||||||
content.update(storages, data: data, updateProperties: updateProperties, type: type)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -65,19 +63,19 @@ public enum Backend2 {
|
|||||||
|
|
||||||
public protocol BackendSceneElement: SceneElement { }
|
public protocol BackendSceneElement: SceneElement { }
|
||||||
|
|
||||||
public class Backend2App: AppStorage {
|
public actor Backend2App: AppStorage{
|
||||||
|
|
||||||
public typealias SceneElementType = BackendSceneElement
|
public typealias SceneElementType = BackendSceneElement
|
||||||
|
|
||||||
public var storage: StandardAppStorage = .init()
|
public var storage: StandardAppStorage = .init()
|
||||||
|
|
||||||
public required init() { }
|
public init(id: String) { }
|
||||||
|
|
||||||
public func run(setup: @escaping () -> Void) {
|
nonisolated public func run(setup: @escaping () -> Void) {
|
||||||
setup()
|
setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func quit() {
|
nonisolated public func quit() {
|
||||||
fatalError("Quit")
|
fatalError("Quit")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
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