meta/Sources/Meta.docc/Tutorials/CreateBackend.md
2024-07-12 21:52:57 +02:00

6.0 KiB

Create a Backend

Learn how to implement a backend.

Overview

In this tutorial, 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.

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.

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:

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.

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.

@_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 or ask a question in the discussions. Feedback on the documentation is appreciated!