Add documentation
This commit is contained in:
parent
da69a573fc
commit
e133b7b8b7
@ -26,12 +26,12 @@ _Meta_ follows the following principles:
|
||||
|
||||
- It is a **declarative** framework, meaning that instead of writing _how_ to construct a user interface, you write _what_ it looks like.
|
||||
- The user interface is treated as a function of its **state**. Instead of directly modifying the UI, modify its state to update views.
|
||||
- Multiple UI frameworks can be used in the same code, but the **selection of the framework** happens when executing the app. This enables the creation of cross-platform UI frameworks combining several UI frameworks rendering always with the same backend.
|
||||
- Multiple UI frameworks can be used in the same code, but the **selection of the framework** happens when executing the app. This enables the creation of cross-platform UI frameworks combining several UI frameworks which render always with the same backend.
|
||||
|
||||
It knows the following layers of UI:
|
||||
|
||||
- An app is the entry point of the executable, containing the windows.
|
||||
- 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, another view, or another renderable component.
|
||||
- Custom renderable components can be used for more restrictive UI elements, such as menus.
|
||||
|
||||
|
||||
@ -8,11 +8,24 @@ _Meta_ follows the following principles:
|
||||
|
||||
- It is a **declarative** framework, meaning that instead of writing _how_ to construct a user interface, you write _what_ it looks like.
|
||||
- The user interface is treated as a function of its **state**. Instead of directly modifying the UI, modify its state to update views.
|
||||
- Multiple UI frameworks can be used in the same code, but the **selection of the framework** happens when executing the app. This enables the creation of cross-platform UI frameworks combining several UI frameworks rendering always with the same backend.
|
||||
- Multiple UI frameworks can be used in the same code, but the **selection of the framework** happens when executing the app. This enables the creation of cross-platform UI frameworks combining several UI frameworks which render always with the same backend.
|
||||
|
||||
It knows the following layers of UI:
|
||||
|
||||
- An app is the entry point of the executable, containing the windows.
|
||||
- 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, another view, or another renderable component.
|
||||
- Custom renderable components can be used for more restrictive UI elements, such as menus.
|
||||
|
||||
## Topics
|
||||
|
||||
### Principles
|
||||
|
||||
- <doc:DeclarativeDesign>
|
||||
- <doc:StateConcept>
|
||||
- <doc:Backends>
|
||||
|
||||
### Tutorials
|
||||
|
||||
- <doc:CreateBackend>
|
||||
- <doc:CreateApp>
|
||||
|
||||
52
Sources/Meta.docc/Principles/Backends.md
Normal file
52
Sources/Meta.docc/Principles/Backends.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Backends
|
||||
|
||||
Multiple UI frameworks can be used in the same code, but the selection of the framework happens when executing the app. This enables the creation of cross-platform UI frameworks combining several UI frameworks which render always with the same backend.
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
It implements at least one type conforming to ``SceneElement`` that can be added to an app's scene.
|
||||
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.
|
||||
|
||||
Learn how to create a backend under <doc:CreateBackend>.
|
||||
|
||||
## Select a Backend for Rendering
|
||||
|
||||
To use a backend in an app, set the correct app storage in the definition of the app.
|
||||
If you want to create a terminal app, use the [TermKitBackend](https://github.com/david-swift/TermKitBackend).
|
||||
|
||||
```swift
|
||||
import TermKitBackend
|
||||
|
||||
@main
|
||||
struct Subtasks: App {
|
||||
|
||||
let id = "io.github.david_swift.Subtasks"
|
||||
var app: TermKitApp! // Render using the TermKitBackend
|
||||
|
||||
var scene: Scene {
|
||||
Window {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## Cross-Platform Apps
|
||||
|
||||
Even though the backend is set to the TermKitBackend, you can use any view conforming to ``AnyView`` and any scene conforming to ``SceneElement`` in your definition.
|
||||
This is enabled by another concept: backends have their own view protocol and their own scene protocol, which conform to ``Widget`` or ``SceneElement``, respectively.
|
||||
All the concrete UI elements specific to a backend conform to the backend's protocol.
|
||||
The conformance to the protocol can therefore be used to identify widgets that should be rendered. If you were to define a platform-independent widget, a so-called convenience widget, make it conform to the ``ConvenienceWidget`` protocol instead, so it will always be rendered.
|
||||
|
||||
The app storage of the backend contains the widget and scene element protocols (``AppStorage/WidgetType`` and ``AppStorage/SceneElementType``) which will be used for rendering.
|
||||
|
||||
## Umbrella Backends
|
||||
|
||||
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 alternative to the ``App`` protocol ensures that the right backend is selected on the right platform.
|
||||
164
Sources/Meta.docc/Principles/DeclarativeDesign.md
Normal file
164
Sources/Meta.docc/Principles/DeclarativeDesign.md
Normal file
@ -0,0 +1,164 @@
|
||||
# Declarative Design
|
||||
|
||||
_Meta_ is a declarative framework, meaning that instead of writing _how_ to construct a user interface, you write _what_ it looks like.
|
||||
|
||||
## Declarative Programming
|
||||
|
||||
When programming in an imperative style, you write a series of commands that will be executed in order to achieve a desired result. As an example, you could define an array in Swift in the following way:
|
||||
|
||||
```swift
|
||||
var array: [Int] = .init()
|
||||
|
||||
array.append(5)
|
||||
array.append(10)
|
||||
array.append(2)
|
||||
array.append(3)
|
||||
|
||||
// array: [5, 10, 2, 3]
|
||||
```
|
||||
|
||||
If you prefer a more declarative approach, the same result can be achieved by instructing the compiler directly what result to achieve:
|
||||
|
||||
```swift
|
||||
let array = [5, 10, 2, 3]
|
||||
|
||||
// array: [5, 10, 2, 3]
|
||||
```
|
||||
|
||||
The comparison between the two solutions shows the most important properties of a more imperative and a more declarative programming style.
|
||||
|
||||
| Imperative | Declarative |
|
||||
|------------------------------------------------------------|---------------------------------------------------|
|
||||
| Better visible _how_ the result is achieved (control flow) | Not directly visible how the result is achieved |
|
||||
| Result must be mentally constructed based on the commands | Better visible _what_ the result is (readability) |
|
||||
|
||||
Higher readability leads to code that is easier to understand, maintain, and extend. However, declarative code can be less performant as it has to be translated into imperative code.
|
||||
|
||||
## Declarative Programming and User Interfaces
|
||||
|
||||
User interfaces are often constructed in a quite declarative way. This is enabled by domain-specific languages (which can be used for the definition of the UI only), such as:
|
||||
|
||||
- HTML for web pages
|
||||
- [XAML](https://learn.microsoft.com/en/windows/apps/winui/winui3/desktop-winui3-app-with-basic-interop) for Windows apps
|
||||
- [JetPack Compose](https://developer.android.com/develop/ui/compose) for Android apps
|
||||
- [Blueprint](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/) for GNOME apps
|
||||
- [SwiftUI](https://developer.apple.com/xcode/swiftui/) for Apple platforms
|
||||
|
||||
Swift is a general-purpose language, allowing the definition of simple domain-specific "languages" within the programming language, with a feature called [result builders](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/advancedoperators/#Result-Builders). Result builders are used in SwiftUI as well as in the _Meta_ package to create a language that allows the declarative declaration of UIs.
|
||||
|
||||
As an example, you could write an app using [TermKit](https://github.com/migueldeicaza/TermKit) imperatively in the following way:
|
||||
|
||||
```swift
|
||||
let win = Window("Window")
|
||||
win.fill()
|
||||
|
||||
let label = Label("Label")
|
||||
|
||||
let button = Button("Button") {
|
||||
print("Button clicked")
|
||||
}
|
||||
|
||||
win.addSubview(label)
|
||||
win.addSubview(button)
|
||||
|
||||
button.y = .bottom(of: label)
|
||||
```
|
||||
|
||||
When you create a _backend_ for terminal UIs, you can use the imperative [TermKit](https://github.com/migueldeicaza/TermKit) package in combination with the declarative _Meta_ package to create the following domain-specfic language (this creates the same UI as the code above, also calling the same commands after the "translation" to imperative code):
|
||||
|
||||
```swift
|
||||
Window {
|
||||
Label("Label")
|
||||
Button("Button") {
|
||||
print("Button clicked")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Elements of the User Interface
|
||||
|
||||
_Meta_ knows different levels of UI. The app is the entry point of the executable. It contains multiple scene elements (e.g., windows on desktop systems, but can also be, e.g., menu bars - simply anything "top-level"), which may contain other scene elements, views, or custom renderable elements (e.g., menus). All of those layers, except for the app layer, are defined using their own domain-specific language.
|
||||
|
||||
The following code shows all of the available levels of UI for a typical desktop _backend_ (but all the elements are backend-specific):
|
||||
|
||||
```swift
|
||||
@main
|
||||
struct AwesomeApp: App { // The app (no DSL)
|
||||
|
||||
let id = "io.github.david_swift.AwesomeApp"
|
||||
var app: GenericDesktopApp!
|
||||
|
||||
var scene: Scene { // The scene DSL
|
||||
Window("Awesome App") { // The view DSL
|
||||
ContentView()
|
||||
.padding(10)
|
||||
Menu { // A DSL for custom renderable elements
|
||||
Button("Hello") { print("Hello") }
|
||||
Button("World") { print("World") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
In the <doc:CreateBackend> tutorial, you will get more familiar with the different levels of UI.
|
||||
|
||||
A domain-specific language in _Meta_ consists of the following definitions:
|
||||
|
||||
- A result builder translates the domain-specific language into an array (``ViewBuilder`` for views, ``SceneBuilder`` for scenes, ``Builder`` for custom renderable elements).
|
||||
- A protocol for elements of the domain (``AnyView`` for views, ``SceneElement`` for scenes, ``Renderable`` for custom renderable elements). When constructing or updating a user interface, functions required by this protocol will be called. The array is "translated" into the actual UI.
|
||||
- A storage object persists between updates and saves data concerning a UI element (``ViewStorage`` for views, ``SceneStorage`` for scenes, ``RenderableStorage`` for custom renderable elements). This is required as the UI elements' definitions in the DSL are re-rendered with each update.
|
||||
|
||||
When creating a backend, you define platform-specific UI elements conforming to the protocol for this type of UI element and manage their "translation" into imperative code using the storage object.
|
||||
|
||||
## Split Declarative Definitions
|
||||
|
||||
There are two ways to split complex definitions using the DSL, for improving the readability or for reusing components.
|
||||
|
||||
First, it is possible to create additional computed variables (such as `scene` above) holding a DSL, and reference them in other DSLs.
|
||||
|
||||
```swift
|
||||
@main
|
||||
struct AwesomeApp: App {
|
||||
|
||||
let id = "io.github.david_swift.AwesomeApp"
|
||||
var app: GenericDesktopApp!
|
||||
|
||||
var scene: Scene {
|
||||
MenuBar {
|
||||
MainMenu()
|
||||
}
|
||||
windows
|
||||
}
|
||||
|
||||
@SceneBuilder // Use the builder for the specific element type, see the list above
|
||||
var windows: Scene {
|
||||
Window("Awesome App") {
|
||||
ContentView()
|
||||
}
|
||||
Window("Extensions", spawn: 0) {
|
||||
ExtensionsView()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Second, you can define custom UI elements (e.g. views such as `ContentView`).
|
||||
|
||||
```swift
|
||||
struct ContentView: View {
|
||||
|
||||
var view: Body {
|
||||
Label("Hello, world!")
|
||||
Button("More Information") {
|
||||
print("Hello, world!")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
This technique is used by _umbrella backends_ in order to provide UI elements that render on multiple platforms (more information under <doc:Backends>),
|
||||
and can be used in combination with the <doc:StateConcept> system to manage the information displayed in parts of the UI.
|
||||
165
Sources/Meta.docc/Principles/StateConcept.md
Normal file
165
Sources/Meta.docc/Principles/StateConcept.md
Normal file
@ -0,0 +1,165 @@
|
||||
# State
|
||||
|
||||
The user interface is treated as a function of its state. Instead of directly modifying the UI, modify its state to update views.
|
||||
|
||||
## Reactive Programming
|
||||
|
||||
When using _Meta_, not only the structure of the user interface is developed in a declarative way (<doc:DeclarativeDesign>), the updating system is as well.
|
||||
You define all the information used for processing the user interface in the app's structure or custom view structures.
|
||||
Whenever information gets updated, the user interface re-renders automatically.
|
||||
|
||||
## State
|
||||
|
||||
An app is built around data it can read and modify. This data is called state. As an example, if you have a simple counter app, there is one piece of state: the count.
|
||||
|
||||
```swift
|
||||
@main
|
||||
struct CounterApp: App {
|
||||
|
||||
let id = "io.github.david_swift.CounterApp"
|
||||
var app: SomePlatformApp!
|
||||
|
||||
@State private var count = 0 // Initialize state
|
||||
|
||||
var scene: Scene {
|
||||
Window("Awesome App") {
|
||||
HStack {
|
||||
Button(.minus) { count -= 1 } // Modify state
|
||||
Text("\(count)") // Get state
|
||||
Button(.plus) { count += 1 } // Modify state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Whenever one of the buttons gets pressed and updates the state variable, the UI elements automatically get updated (see <doc:DeclarativeDesign> for more information about the elements). As this happens for all the state variables (properties of the view marked with ``State``), and because manual synchronization is challenging, the following rule is very important:
|
||||
|
||||
> Each state variable creates a new source of truth.
|
||||
|
||||
You should have _one_ source of truth for _one_ piece of data. It might help to think about the UI being a function of the state: in the same way as you want to minimize the number of parameters of a function and compute as much as possible, you want to minimize the number of state variables. Therefore, if you want to display double the count, implement the operation while rendering the view:
|
||||
|
||||
```swift
|
||||
Text("\(2 * count)")
|
||||
```
|
||||
|
||||
For more complex operations, computed variables can be helpful.
|
||||
|
||||
```swift
|
||||
@main
|
||||
struct CounterApp: App {
|
||||
|
||||
let id = "io.github.david_swift.CounterApp"
|
||||
var app: SomePlatformApp!
|
||||
|
||||
@State private var count = 0
|
||||
|
||||
var doubleCount: Int {
|
||||
2 * count
|
||||
}
|
||||
|
||||
var scene: Scene {
|
||||
Window("Awesome App") {
|
||||
HStack {
|
||||
Button(.minus) { count -= 1 }
|
||||
Text("\(doubleCount)")
|
||||
Button(.plus) { count += 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## State in Views
|
||||
|
||||
State cannot only be defined for an app definition, but also for a view definition.
|
||||
In order not to violate the "single source of truth" concept, the state should be defined in the least common ancestor of all the views accessing this state.
|
||||
|
||||
You can pass state to child views using two different methods.
|
||||
|
||||
### Read the State in Child Views
|
||||
|
||||
Whenever you want to read, but not modify the state in a child view, use a regular property in the view.
|
||||
|
||||
```swift
|
||||
struct CountView: View {
|
||||
|
||||
var count: Int // The property
|
||||
|
||||
var view: Body {
|
||||
Text("\(count)") // Get the property
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@State private var count = 0
|
||||
|
||||
var view: Body {
|
||||
CountView(count: count) // Pass state to the child view
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Read and Modify the State in Child Views
|
||||
|
||||
If you want to modify the state in a child view, you need to establish two-way data traffic.
|
||||
_Meta_ provides the property wrapper ``Binding`` for this.
|
||||
|
||||
```swift
|
||||
struct IncreaseButton: View {
|
||||
|
||||
@Binding var count: Int // The binding property
|
||||
|
||||
var view: Body {
|
||||
Button(.plus) {
|
||||
count += 1 // Modify the binding
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@State private var count = 0
|
||||
|
||||
var view: Body {
|
||||
IncreaseButton(count: $count) // Pass state or a binding as a binding to the child view using the dollar sign ($)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## Observation
|
||||
|
||||
State can manage either value types (as seen in the examples above), or [observable reference types](https://developer.apple.com/documentation/observation).
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
class TaskModel {
|
||||
|
||||
var tasks: [String] = []
|
||||
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@State private var model = TaskModel()
|
||||
|
||||
var view: Body {
|
||||
TaskList(tasks: $model.tasks)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Observable reference types can be handy when, e.g., synchronizing state with a server.
|
||||
|
||||
## State in Backends
|
||||
|
||||
When creating a backend, you do not have to worry about state. State is fully managed by the _Meta_ package.
|
||||
More information on how to create a backend can be found in the <doc:CreateBackend> tutorial.
|
||||
80
Sources/Meta.docc/Tutorials/CreateApp.md
Normal file
80
Sources/Meta.docc/Tutorials/CreateApp.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Create an App
|
||||
|
||||
Learn how to use a backend for creating your app.
|
||||
|
||||
## Find a Backend
|
||||
|
||||
To develop an app, you first need to find either a backend or an umbrella backend.
|
||||
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 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://github.com/david-swift/TermKitBackend) will be used as a sample backend.
|
||||
|
||||
## Create the User Interface
|
||||
|
||||
Start by defining the app structure.
|
||||
|
||||
```swift
|
||||
import TermKitBackend
|
||||
|
||||
@main
|
||||
struct AppName: App {
|
||||
|
||||
let id = "com.example.AppName"
|
||||
var app: TermKitApp!
|
||||
|
||||
var scene: Scene {
|
||||
Window {
|
||||
Label("Hello, world!")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
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".
|
||||
|
||||
Fill `scene` with the UI definition. More information about the UI elements and the organization of the code is available under <doc:DeclarativeDesign>.
|
||||
What will be relevant is the concept of <doc:StateConcept>.
|
||||
|
||||
## App Storage Functions
|
||||
|
||||
Certain functions are defined on the app storage (here `TermKitApp`). The following ones may be helpful when developing an app:
|
||||
|
||||
- ``AppStorage/addWindow(_:)`` or ``AppStorage/addSceneElement(_:)`` to create and show scene elements (allows multiple instances)
|
||||
- ``AppStorage/showWindow(_:)`` or ``AppStorage/showSceneElement(_:)`` to show scene elements or create a new one if it does not already exist (allows only one instance)
|
||||
- ``AppStorage/quit()`` quits the application
|
||||
|
||||
## Support Multiple Platforms
|
||||
|
||||
To support multiple platforms, import multiple backends into your code.
|
||||
You can mix up views from every backend, but only the one that is selected via the `app` property will render.
|
||||
Therefore, it often makes sense to change the type of the `app` property based on the backend:
|
||||
|
||||
```swift
|
||||
import TermKitBackend
|
||||
|
||||
@main
|
||||
struct AppName: App {
|
||||
|
||||
let id = "com.example.AppName"
|
||||
|
||||
#if os(macOS)
|
||||
var app: MacApp!
|
||||
#else
|
||||
var app: AdwaitaApp!
|
||||
#endif
|
||||
|
||||
var scene: Scene {
|
||||
Window {
|
||||
Label("Hello, world!")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
As this can get complicated, especially because the dependencies should be handled based on the platform as well, the preferred way of supporting multiple backends is to write an umbrella backend (more information under <doc:Backends>).
|
||||
171
Sources/Meta.docc/Tutorials/CreateBackend.md
Normal file
171
Sources/Meta.docc/Tutorials/CreateBackend.md
Normal file
@ -0,0 +1,171 @@
|
||||
# Create a Backend
|
||||
|
||||
Learn how to implement a backend.
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
## Package Manifest
|
||||
|
||||
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.
|
||||
|
||||
```swift
|
||||
let package = Package(
|
||||
name: "TermKitBackend",
|
||||
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
|
||||
products: [
|
||||
.library(
|
||||
name: "TermKitBackend",
|
||||
targets: ["TermKitBackend"]
|
||||
)
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/AparokshaUI/Meta", from: "0.1.0"),
|
||||
.package(url: "https://github.com/david-swift/TermKit", branch: "main")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "TermKitBackend",
|
||||
dependencies: ["TermKit", "Meta"]
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Backend-Specific Protocols
|
||||
|
||||
As mentioned in <doc:Backends>, the backend has to define a backend-specific widget and scene element type.
|
||||
|
||||
```swift
|
||||
import Meta
|
||||
|
||||
public protocol TermKitSceneElement: SceneElement { }
|
||||
public protocol TermKitWidget: Widget { }
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```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
|
||||
|
||||
public struct VStack: Wrapper, TermKitWidget {
|
||||
|
||||
var content: 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()
|
||||
}
|
||||
|
||||
public func container<Storage>(
|
||||
modifiers: [(any AnyView) -> any AnyView],
|
||||
type: Storage.Type
|
||||
) -> ViewStorage where Storage: AppStorage {
|
||||
let storages = content.storages(modifiers: modifiers, type: type) // Get the storages of child views
|
||||
if storages.count == 1 {
|
||||
return .init(storages[0].pointer, content: [.mainContent: storages])
|
||||
}
|
||||
let view = View()
|
||||
for (index, storage) in storages.enumerated() {
|
||||
if let pointer = storage.pointer as? TermKit.View {
|
||||
view.addSubview(pointer)
|
||||
if let previous = (storages[safe: index - 1]?.pointer as? TermKit.View) { // The pointer should be a TermKit view
|
||||
pointer.y = .bottom(of: previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
return .init(view, content: [.mainContent: storages]) // Save storages of child views in the parent's storage for view updates
|
||||
}
|
||||
|
||||
public func update<Storage>(
|
||||
_ storage: ViewStorage,
|
||||
modifiers: [(any AnyView) -> any AnyView],
|
||||
updateProperties: Bool,
|
||||
type: Storage.Type
|
||||
) where Storage: AppStorage {
|
||||
guard let storages = storage.content[.mainContent] else {
|
||||
return
|
||||
}
|
||||
content.update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type) // Update the storages of child views
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Correct Updating
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
- _Always_ update view content (using ``AnyView/updateStorage(_:modifiers:updateProperties:type:)`` or ``Swift/Array/storages(modifiers:type:)``). Child views may contain own state.
|
||||
- _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.
|
||||
|
||||
## The App Storage
|
||||
|
||||
An app storage object in the app definition determines which backend to use for rendering.
|
||||
Therefore, it must contain information about the scene element and widget types, as well as the wrapper widget.
|
||||
|
||||
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.
|
||||
The quit funtion should terminate the app.
|
||||
|
||||
```swift
|
||||
@_exported import Meta // Export the Meta package
|
||||
import TermKit
|
||||
|
||||
public class TermKitApp: AppStorage {
|
||||
|
||||
public typealias SceneElementType = TermKitSceneElement
|
||||
public typealias WidgetType = TermKitWidget
|
||||
public typealias WrapperType = VStack
|
||||
|
||||
public var app: () -> any App
|
||||
public var storage: StandardAppStorage = .init()
|
||||
|
||||
public required init(id: String, app: @escaping () -> any App) {
|
||||
self.app = app
|
||||
}
|
||||
|
||||
public func run(setup: @escaping () -> Void) {
|
||||
Application.prepare()
|
||||
setup()
|
||||
Application.run()
|
||||
}
|
||||
|
||||
public func quit() {
|
||||
Application.shutdown()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now, you can start implementing scene elements (windows or other "top-level containers"), views, and custom renderable elements.
|
||||
Remember following the instructions for correct updating above for all of the UI element types.
|
||||
|
||||
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!
|
||||
@ -49,7 +49,7 @@ public enum StateManager {
|
||||
/// Update all of the views.
|
||||
/// - Parameter force: Whether to force all views to update.
|
||||
///
|
||||
/// Nothing happens if ``UpdateManager/blockUpdates`` is true.
|
||||
/// Nothing happens if ``StateManager/blockUpdates`` is true.
|
||||
public static func updateViews(force: Bool = false) {
|
||||
if !blockUpdates {
|
||||
for handler in updateHandlers {
|
||||
@ -60,7 +60,7 @@ public enum StateManager {
|
||||
|
||||
/// 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.
|
||||
public static func addUpdateHandler(handler: @escaping (Bool) -> Void) {
|
||||
static func addUpdateHandler(handler: @escaping (Bool) -> Void) {
|
||||
updateHandlers.append(handler)
|
||||
}
|
||||
|
||||
|
||||
@ -119,7 +119,7 @@ extension Array where Element == Renderable {
|
||||
|
||||
/// Update a collection of renderable elements.
|
||||
/// - Parameters:
|
||||
/// - storage: The collection of renderable storages.
|
||||
/// - storages: The collection of renderable storages.
|
||||
/// - updateProperties: Whether to update properties.
|
||||
/// - type: The type of the renderable element.
|
||||
/// - fields: Additional information.
|
||||
|
||||
@ -14,6 +14,7 @@ import Observation
|
||||
/// struct Test: App {
|
||||
///
|
||||
/// let id = "io.github.AparokshaUI.TestApp"
|
||||
/// var app: BackendApp!
|
||||
///
|
||||
/// var scene: Scene {
|
||||
/// WindowScene()
|
||||
@ -24,7 +25,7 @@ import Observation
|
||||
///
|
||||
public protocol App {
|
||||
|
||||
/// The app storage typ.
|
||||
/// The app storage type.
|
||||
associatedtype Storage: AppStorage
|
||||
|
||||
/// The app's application ID.
|
||||
@ -54,8 +55,9 @@ extension App {
|
||||
}
|
||||
|
||||
/// Initialize and get the app with the app storage.
|
||||
/// - Returns: The app instance.
|
||||
///
|
||||
/// To run the app, call the ``AppStorage/run(automaticSetup:manualSetup:)`` function.
|
||||
/// To run the app, call the ``AppStorage/run(setup:)`` function.
|
||||
public static func setupApp() -> Self {
|
||||
var appInstance = self.init()
|
||||
appInstance.app = Storage(id: appInstance.id) { appInstance }
|
||||
|
||||
@ -7,11 +7,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// The ``Builder`` is a result builder for scenes.
|
||||
/// The ``Builder`` is a result builder for custom data.
|
||||
@resultBuilder
|
||||
public enum Builder<RenderableType> {
|
||||
|
||||
/// A component used in the ``MenuBuilder``.
|
||||
/// A component used in the ``Builder``.
|
||||
public enum Component {
|
||||
|
||||
/// A renderable element as a component.
|
||||
@ -22,20 +22,20 @@ public enum Builder<RenderableType> {
|
||||
}
|
||||
|
||||
/// Build combined results from statement blocks.
|
||||
/// - Parameter components: The components.
|
||||
/// - Parameter elements: The components.
|
||||
/// - Returns: The components in a component.
|
||||
public static func buildBlock(_ elements: Component...) -> Component {
|
||||
.components(elements)
|
||||
}
|
||||
|
||||
/// Translate an element into a ``MenuBuilder.Component``.
|
||||
/// Translate an element into a ``Builder/Component``.
|
||||
/// - Parameter element: The element to translate.
|
||||
/// - Returns: A component created from the element.
|
||||
public static func buildExpression(_ element: RenderableType) -> Component {
|
||||
.element(element)
|
||||
}
|
||||
|
||||
/// Translate an array of elements into a ``MenuBuilder.Component``.
|
||||
/// Translate an array of elements into a ``Builder/Component``.
|
||||
/// - Parameter elements: The elements to translate.
|
||||
/// - Returns: A component created from the element.
|
||||
public static func buildExpression(_ elements: [RenderableType]) -> Component {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
// Created by david-swift on 26.05.24.
|
||||
//
|
||||
|
||||
/// Store a reference to a rendered view in a view storage.
|
||||
/// Store a reference to a rendered element in a renderable storage.
|
||||
public class RenderableStorage {
|
||||
|
||||
/// The pointer.
|
||||
|
||||
@ -22,20 +22,20 @@ public enum SceneBuilder {
|
||||
}
|
||||
|
||||
/// Build combined results from statement blocks.
|
||||
/// - Parameter components: The components.
|
||||
/// - Parameter elements: The components.
|
||||
/// - Returns: The components in a component.
|
||||
public static func buildBlock(_ elements: Component...) -> Component {
|
||||
.components(elements)
|
||||
}
|
||||
|
||||
/// Translate an element into a ``SceneBuilder.Component``.
|
||||
/// Translate an element into a ``SceneBuilder/Component``.
|
||||
/// - Parameter element: The element to translate.
|
||||
/// - Returns: A component created from the element.
|
||||
public static func buildExpression(_ element: any SceneElement) -> Component {
|
||||
.element(element)
|
||||
}
|
||||
|
||||
/// Translate an array of elements into a ``SceneBuilder.Component``.
|
||||
/// Translate an array of elements into a ``SceneBuilder/Component``.
|
||||
/// - Parameter elements: The elements to translate.
|
||||
/// - Returns: A component created from the element.
|
||||
public static func buildExpression(_ elements: [any SceneElement]) -> Component {
|
||||
|
||||
@ -19,6 +19,7 @@ public protocol SceneElement {
|
||||
/// Update the stored content.
|
||||
/// - Parameters:
|
||||
/// - storage: The storage to update.
|
||||
/// - app: The app storage.
|
||||
/// - updateProperties: Whether to update the view's properties.
|
||||
func update<Storage>(_ storage: SceneStorage, app: Storage, updateProperties: Bool) where Storage: AppStorage
|
||||
|
||||
|
||||
@ -22,20 +22,20 @@ public enum ViewBuilder {
|
||||
}
|
||||
|
||||
/// Build combined results from statement blocks.
|
||||
/// - Parameter components: The components.
|
||||
/// - Parameter elements: The components.
|
||||
/// - Returns: The components in a component.
|
||||
public static func buildBlock(_ elements: Component...) -> Component {
|
||||
.components(elements)
|
||||
}
|
||||
|
||||
/// Translate an element into a ``ViewBuilder.Component``.
|
||||
/// Translate an element into a ``ViewBuilder/Component``.
|
||||
/// - Parameter element: The element to translate.
|
||||
/// - Returns: A component created from the element.
|
||||
public static func buildExpression(_ element: AnyView) -> Component {
|
||||
.element(element)
|
||||
}
|
||||
|
||||
/// Translate an array of elements into a ``ViewBuilder.Component``.
|
||||
/// Translate an array of elements into a ``ViewBuilder/Component``.
|
||||
/// - Parameter elements: The elements to translate.
|
||||
/// - Returns: A component created from the element.
|
||||
public static func buildExpression(_ elements: [AnyView]) -> Component {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user