Update documentation
This commit is contained in:
parent
559caa6f55
commit
cd214f2a7b
@ -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>.
|
||||
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.
|
||||
Most importantly, a backend usually 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.
|
||||
|
||||
@ -24,8 +24,7 @@ import TermKitBackend
|
||||
@main
|
||||
struct Subtasks: App {
|
||||
|
||||
let id = "io.github.david_swift.Subtasks"
|
||||
var app: TermKitApp! // Render using the TermKitBackend
|
||||
let app: TermKitApp() // Render using the TermKitBackend
|
||||
|
||||
var scene: Scene {
|
||||
Window {
|
||||
@ -50,4 +49,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.
|
||||
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.
|
||||
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.
|
||||
|
||||
@ -86,8 +86,7 @@ The following code shows all of the available levels of UI for a typical desktop
|
||||
@main
|
||||
struct AwesomeApp: App { // The app (no DSL)
|
||||
|
||||
let id = "io.github.david_swift.AwesomeApp"
|
||||
var app: GenericDesktopApp!
|
||||
let app = SomeBackendApp()
|
||||
|
||||
var scene: Scene { // The scene DSL
|
||||
Window("Awesome App") { // The view DSL
|
||||
@ -121,8 +120,7 @@ First, it is possible to create additional computed variables (such as `scene` a
|
||||
@main
|
||||
struct AwesomeApp: App {
|
||||
|
||||
let id = "io.github.david_swift.AwesomeApp"
|
||||
var app: GenericDesktopApp!
|
||||
let app = SomeBackendApp()
|
||||
|
||||
var scene: Scene {
|
||||
MenuBar {
|
||||
|
||||
@ -16,8 +16,7 @@ An app is built around data it can read and modify. This data is called state. A
|
||||
@main
|
||||
struct CounterApp: App {
|
||||
|
||||
let id = "io.github.david_swift.CounterApp"
|
||||
var app: SomePlatformApp!
|
||||
let app = SomeBackendApp()
|
||||
|
||||
@State private var count = 0 // Initialize state
|
||||
|
||||
@ -50,8 +49,7 @@ For more complex operations, computed variables can be helpful.
|
||||
@main
|
||||
struct CounterApp: App {
|
||||
|
||||
let id = "io.github.david_swift.CounterApp"
|
||||
var app: SomePlatformApp!
|
||||
let app = SomeBackendApp()
|
||||
|
||||
@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 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.
|
||||
In this tutorial, [TermKitBackend](https://git.aparoksha.dev/david-swift/term-kit-backend) will be used as a sample backend.
|
||||
|
||||
## Create the User Interface
|
||||
|
||||
@ -22,8 +22,7 @@ import TermKitBackend
|
||||
@main
|
||||
struct AppName: App {
|
||||
|
||||
let id = "com.example.AppName"
|
||||
var app: TermKitApp!
|
||||
let app = TermKitApp()
|
||||
|
||||
var scene: Scene {
|
||||
Window {
|
||||
@ -34,7 +33,7 @@ struct AppName: App {
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
The app property holds the platform-specific application object.
|
||||
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>.
|
||||
@ -60,12 +59,10 @@ import TermKitBackend
|
||||
@main
|
||||
struct AppName: App {
|
||||
|
||||
let id = "com.example.AppName"
|
||||
|
||||
#if os(macOS)
|
||||
var app: MacApp!
|
||||
let app = MacApp(id: "dev.aparoksha.AppName")
|
||||
#else
|
||||
var app: AdwaitaApp!
|
||||
let app = AdwaitaApp(id: "dev.aparoksha.AppName")
|
||||
#endif
|
||||
|
||||
var scene: Scene {
|
||||
|
||||
@ -4,7 +4,7 @@ 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.
|
||||
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.
|
||||
General information can be found in the <doc:Backends> article.
|
||||
|
||||
## Package Manifest
|
||||
@ -13,6 +13,10 @@ 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
|
||||
// swift-tools-version: 6.0
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "TermKitBackend",
|
||||
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
|
||||
@ -29,59 +33,265 @@ let package = Package(
|
||||
targets: [
|
||||
.target(
|
||||
name: "TermKitBackend",
|
||||
dependencies: ["TermKit", "Meta"]
|
||||
dependencies: [
|
||||
"TermKit",
|
||||
.product(name: "Meta", package: "meta")
|
||||
]
|
||||
)
|
||||
]
|
||||
],
|
||||
swiftLanguageModes: [.v5]
|
||||
)
|
||||
```
|
||||
|
||||
## Backend-Specific Protocols
|
||||
## Backend-Specific Scene Type
|
||||
|
||||
As mentioned in <doc:Backends>, the backend has to define a backend-specific scene element type.
|
||||
Often, it is sensible to define a widget type for regular views.
|
||||
An app can contain scenes from multiple backends. To determine which scenes to render, a backend-specific scene protocol is required.
|
||||
Simply create a protocol which conforms to the ``SceneElement`` protocol.
|
||||
|
||||
```swift
|
||||
// TermKitSceneElement.swift
|
||||
|
||||
import Meta
|
||||
|
||||
public protocol TermKitSceneElement: SceneElement { }
|
||||
public protocol TermKitWidget: Widget { }
|
||||
```
|
||||
|
||||
## The Wrapper Widget
|
||||
## The App Storage
|
||||
|
||||
In this section, the widget type for regular views will be extended so that it can be used for rendering.
|
||||
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(_:)``).
|
||||
|
||||
With _Meta_, arrays of ``AnyView`` have to be able to be converted into a single widget.
|
||||
This allows definitions such as the following one:
|
||||
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
|
||||
Window {
|
||||
Label("Hello")
|
||||
Label("World")
|
||||
// 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 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.
|
||||
## 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
|
||||
import Meta
|
||||
// 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 { }
|
||||
```
|
||||
|
||||
The widget type (``TermKitMainView/WidgetType``) is not enough for working with view types.
|
||||
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.
|
||||
- ``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
|
||||
|
||||
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
|
||||
// VStack.swift
|
||||
|
||||
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
|
||||
public init(@ViewBuilder content: @escaping () -> Body) {
|
||||
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>(
|
||||
data: WidgetData,
|
||||
type: Data.Type
|
||||
) -> ViewStorage where Data: ViewRenderData {
|
||||
let storages = content.storages(data: data, type: type) // Get the storages of child views
|
||||
let storages = content.storages(data: data, type: type)
|
||||
if storages.count == 1 {
|
||||
return .init(storages[0].pointer, content: [.mainContent: storages])
|
||||
}
|
||||
@ -89,14 +299,29 @@ public struct VStack: Wrapper, TermKitWidget {
|
||||
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
|
||||
if let previous = (storages[safe: index - 1]?.pointer as? 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
|
||||
return .init(view, content: [.mainContent: storages])
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
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>(
|
||||
_ storage: ViewStorage,
|
||||
data: WidgetData,
|
||||
@ -106,80 +331,294 @@ public struct VStack: Wrapper, TermKitWidget {
|
||||
guard let storages = storage.content[.mainContent] else {
|
||||
return
|
||||
}
|
||||
content.update(storages, data: data, updateProperties: updateProperties, type: type) // Update the storages of child views
|
||||
content.update(storages, data: data, updateProperties: updateProperties, type: type)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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()
|
||||
let storage = ViewStorage(view)
|
||||
update(storage, data: data, updateProperties: true, type: type)
|
||||
return storage
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```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)
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Correct Updating
|
||||
If the storage for the current condition has already been initialized, this method calls the children's update function.
|
||||
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.
|
||||
|
||||
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.
|
||||
Assign the correct view to the native representation of the either view if the condition has changed.
|
||||
|
||||
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:
|
||||
### The View Render Data
|
||||
|
||||
- _Always_ update view content (using ``AnyView/updateStorage(_:data:updateProperties:type:)`` or ``Swift/Array/storages(data: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.
|
||||
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.
|
||||
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!
|
||||
|
||||
### The Render Data Type
|
||||
|
||||
Now, define a view render data type for the main views.
|
||||
Let's create the view context type for our main view context:
|
||||
|
||||
```swift
|
||||
public enum MainViewType: ViewRenderData {
|
||||
// TermKitMainView.swift
|
||||
|
||||
public enum TermKitMainView: ViewRenderData {
|
||||
|
||||
public typealias WidgetType = TermKitWidget
|
||||
public typealias WrapperType = VStack
|
||||
public typealias EitherViewType = EitherView
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
### A Regular Widget
|
||||
|
||||
## The App Storage
|
||||
Regular widgets are much simpler to implement. Here, we will implement a simple label widget.
|
||||
|
||||
An app storage object in the app definition determines which backend to use for rendering.
|
||||
Therefore, it must contain information about the scene element.
|
||||
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.
|
||||
One can set the ``Widget/initializeWidget()-9ut4i`` function instead and mark the properties as widget properties with the following property wrappers:
|
||||
|
||||
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.
|
||||
- ``ViewProperty`` for child views
|
||||
- ``BindingProperty`` for bindings (see <doc:StateConcept>)
|
||||
- ``Property`` for other properties (labels, closures, etc.)
|
||||
|
||||
First, create the widget's structure without any backend logic:
|
||||
|
||||
```swift
|
||||
@_exported import Meta // Export the Meta package
|
||||
// Label.swift
|
||||
|
||||
import TermKit
|
||||
|
||||
public class TermKitApp: AppStorage {
|
||||
public struct Label: TermKitWidget {
|
||||
|
||||
public typealias SceneElementType = TermKitSceneElement
|
||||
var label: String
|
||||
|
||||
public var storage: StandardAppStorage = .init()
|
||||
|
||||
public required init(id: String) { }
|
||||
|
||||
public func run(setup: @escaping () -> Void) {
|
||||
Application.prepare()
|
||||
setup()
|
||||
Application.run()
|
||||
public init(_ label: String) {
|
||||
self.label = label
|
||||
}
|
||||
|
||||
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)
|
||||
var view: Body
|
||||
|
||||
public init(@ViewBuilder content: @escaping () -> Body) { // Use the view builder
|
||||
self.view = content()
|
||||
}
|
||||
|
||||
public func initializeWidget() -> Any {
|
||||
TermKit.Frame()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
Define the type of the view context with the `subview` property.
|
||||
|
||||
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.
|
||||
Remember not to use this property wrapper in the wrapper widget.
|
||||
|
||||
### 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,8 +11,7 @@
|
||||
/// @main
|
||||
/// struct Test: App {
|
||||
///
|
||||
/// let id = "io.github.AparokshaUI.TestApp"
|
||||
/// var app: BackendApp!
|
||||
/// let app = BackendApp()
|
||||
///
|
||||
/// var scene: Scene {
|
||||
/// WindowScene()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user