Make lists dynamic

Make one @State always reference the same value
This is important when views with closures appear after the first render
This commit is contained in:
david-swift 2024-01-01 16:08:17 +01:00
parent 37252f7b8f
commit 1c50b3b923
13 changed files with 192 additions and 25 deletions

View File

@ -48,6 +48,7 @@
## Classes
- [GTUIApp](classes/GTUIApp.md)
- [State.Content](classes/State.Content.md)
- [State.Storage](classes/State.Storage.md)
- [ViewStorage](classes/ViewStorage.md)
- [WindowStorage](classes/WindowStorage.md)

View File

@ -0,0 +1,16 @@
**CLASS**
# `State.Content`
A class storing the state's content.
## Properties
### `storage`
The storage.
## Methods
### `init(storage:)`
Initialize the content.
- Parameter storage: The storage.

View File

@ -5,6 +5,6 @@
An interface for accessing `State` without specifying the generic type.
## Properties
### `value`
### `content`
The type-erased value.
The class storing the value.

View File

@ -17,6 +17,10 @@ The content.
The identifier of the selected element.
### `elementsID`
The identifier of the elements storage.
## Methods
### `init(_:selection:content:)`
@ -39,11 +43,27 @@ Get a view storage.
- Parameter modifiers: Modify views before being updated.
- Returns: The view storage.
### `updateList(box:content:modifiers:)`
Update the list's content and selection.
- Parameters:
- box: The list box.
- content: The content's view storage.
- modifiers: The view modifiers.
### `updateSelection(box:)`
Update the list's selection.
- Parameter box: The list box.
### `getWidget(element:modifiers:)`
Get the view storage of an element.
- Parameters:
- element: The element.
- modifiers: The modifiers.
- Returns: The view storage.
### `sidebarStyle()`
Add the "navigation-sidebar" style class.

View File

@ -13,7 +13,7 @@ Access the stored value. This updates the views when being changed.
Get the value as a binding using the `$` prefix.
### `storage`
### `content`
The stored value.

View File

@ -18,12 +18,19 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/AparokshaUI/Libadwaita", from: "0.1.0")
.package(url: "https://github.com/AparokshaUI/Libadwaita", from: "0.1.0"),
.package(
url: "https://github.com/david-swift/LevenshteinTransformations",
from: "0.1.1"
)
],
targets: [
.target(
name: "Adwaita",
dependencies: [.product(name: "Libadwaita", package: "Libadwaita")]
dependencies: [
.product(name: "Libadwaita", package: "Libadwaita"),
.product(name: "LevenshteinTransformations", package: "LevenshteinTransformations")
]
),
.executableTarget(
name: "Swift Adwaita Demo",

View File

@ -115,6 +115,7 @@ I recommend using the [template repository](https://github.com/AparokshaUI/Adwai
### Dependencies
- [Libadwaita][18] licensed under the [GPL-3.0 license][19]
- [Levenshtein Transformations](https://github.com/david-swift/LevenshteinTransformations) licensed under the [MIT license](https://github.com/david-swift/LevenshteinTransformations/blob/main/LICENSE.md)
### Other Thanks
- The [contributors][20]

View File

@ -5,20 +5,24 @@
// Created by david-swift on 06.08.23.
//
import Foundation
/// A property wrapper for properties in a view that should be stored throughout view updates.
@propertyWrapper
public struct State<Value>: StateProtocol {
// swiftlint:disable force_cast
/// Access the stored value. This updates the views when being changed.
public var wrappedValue: Value {
get {
storage.value
content.storage.value as! Value
}
nonmutating set {
storage.value = newValue
content.storage.value = newValue
Self.updateViews()
}
}
// swiftlint:enable force_cast
/// Get the value as a binding using the `$` prefix.
public var projectedValue: Binding<Value> {
@ -30,7 +34,8 @@ public struct State<Value>: StateProtocol {
}
/// The stored value.
private let storage: Storage<Value>
public let content: State<Any>.Content
/// The value with an erased type.
public var value: Any {
get {
@ -38,7 +43,7 @@ public struct State<Value>: StateProtocol {
}
nonmutating set {
if let newValue = newValue as? Value {
storage.value = newValue
content.storage.value = newValue
}
}
}
@ -47,19 +52,33 @@ public struct State<Value>: StateProtocol {
/// - Parameters:
/// - wrappedValue: The wrapped value.
public init(wrappedValue: Value) {
storage = .init(value: wrappedValue)
content = .init(storage: .init(value: wrappedValue))
}
/// A class storing the state's content.
public class Content {
/// The storage.
public var storage: Storage
/// Initialize the content.
/// - Parameter storage: The storage.
public init(storage: Storage) {
self.storage = storage
}
}
/// A class storing the value.
class Storage<StoredValue> {
public class Storage {
/// The stored value.
var value: StoredValue
public var value: Any
/// Initialize the storage.
/// - Parameters:
/// - value: The value.
init(value: StoredValue) {
public init(value: Any) {
self.value = value
}

View File

@ -8,7 +8,7 @@
/// An interface for accessing `State` without specifying the generic type.
public protocol StateProtocol {
/// The type-erased value.
var value: Any { get nonmutating set }
/// The class storing the value.
var content: State<Any>.Content { get }
}

View File

@ -5,6 +5,7 @@
// Created by david-swift on 25.09.23.
//
import LevenshteinTransformations
import Libadwaita
/// A list box widget.
@ -17,6 +18,9 @@ public struct List<Element>: Widget where Element: Identifiable {
/// The identifier of the selected element.
@Binding var selection: Element.ID
/// The identifier of the elements storage.
let elementsID = "elements"
/// Initialize `List`.
/// - Parameters:
/// - elements: The elements.
@ -38,7 +42,12 @@ public struct List<Element>: Widget where Element: Identifiable {
/// - modifiers: Modify views before being updated.
public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) {
if let box = storage.view as? ListBox {
updateSelection(box: box)
var content: [ViewStorage] = storage.content[.mainContent] ?? []
updateList(box: box, content: .init { content } set: { content = $0 }, modifiers: modifiers)
storage.content[.mainContent] = content
for (index, element) in elements.enumerated() {
self.content(element).widget(modifiers: modifiers).update(content[index], modifiers: modifiers)
}
}
}
@ -48,21 +57,45 @@ public struct List<Element>: Widget where Element: Identifiable {
public func container(modifiers: [(View) -> View]) -> ViewStorage {
let box: ListBox = .init()
var content: [ViewStorage] = []
for element in elements {
let widget = self.content(element).widget(modifiers: modifiers).container(modifiers: modifiers)
_ = box.append(widget.view)
content.append(widget)
}
updateList(box: box, content: .init { content } set: { content = $0 }, modifiers: modifiers)
_ = box.handler {
let selection = box.getSelectedRow()
if let id = elements[safe: selection]?.id {
if let id = (box.fields[elementsID] as? [Element] ?? elements)[safe: selection]?.id {
self.selection = id
}
}
updateSelection(box: box)
return .init(box, content: [.mainContent: content])
}
/// Update the list's content and selection.
/// - Parameters:
/// - box: The list box.
/// - content: The content's view storage.
/// - modifiers: The view modifiers.
func updateList(box: ListBox, content: Binding<[ViewStorage]>, modifiers: [(View) -> View]) {
let old = box.fields[elementsID] as? [Element] ?? []
old.identifiableTransform(
to: elements,
functions: .init { index, element in
let widget = getWidget(element: element, modifiers: modifiers)
_ = box.remove(at: index)
_ = box.insert(widget.view, at: index)
content.wrappedValue.remove(at: index)
content.wrappedValue.insert(widget, at: index)
} delete: { index in
_ = box.remove(at: index)
content.wrappedValue.remove(at: index)
updateSelection(box: box)
} insert: { index, element in
let widget = getWidget(element: element, modifiers: modifiers)
_ = box.insert(widget.view, at: index)
content.wrappedValue.insert(widget, at: index)
}
)
box.fields[elementsID] = elements
updateSelection(box: box)
}
/// Update the list's selection.
/// - Parameter box: The list box.
func updateSelection(box: ListBox) {
@ -71,6 +104,15 @@ public struct List<Element>: Widget where Element: Identifiable {
}
}
/// Get the view storage of an element.
/// - Parameters:
/// - element: The element.
/// - modifiers: The modifiers.
/// - Returns: The view storage.
func getWidget(element: Element, modifiers: [(View) -> View]) -> ViewStorage {
self.content(element).widget(modifiers: modifiers).container(modifiers: modifiers)
}
/// Add the "navigation-sidebar" style class.
public func sidebarStyle() -> View {
style("navigation-sidebar")

View File

@ -36,8 +36,8 @@ public struct StateWrapper: Widget {
/// - modifiers: Modify views before being updated.
public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) {
for property in state {
if let value = storage.state[property.key]?.value {
property.value.value = value
if let storage = storage.state[property.key]?.content.storage {
property.value.content.storage = storage
}
}
if let storage = storage.content[.mainContent]?.first {

56
Tests/ListDemo.swift Normal file
View File

@ -0,0 +1,56 @@
//
// ListDemo.swift
// Adwaita
//
// Created by david-swift on 01.01.24.
//
// swiftlint:disable missing_docs
import Adwaita
import Foundation
struct ListDemo: View {
@State private var items: [Element] = []
@State private var selectedItem = ""
var view: Body {
HStack {
Button("Add Row") {
let element = Element(id: UUID().uuidString)
items.append(element)
selectedItem = element.id
}
Button("Delete Selected Row") {
let index = items.firstIndex { $0.id == selectedItem }
items = items.filter { $0.id != selectedItem }
selectedItem = items[safe: index]?.id ?? items[safe: index ?? 0 - 1]?.id ?? items.first?.id ?? ""
}
}
.padding()
.style("linked")
.halign(.center)
if !items.isEmpty {
List(items, selection: $selectedItem) { item in
HStack {
Text("\(item.id)")
.hexpand()
}
.padding()
}
.valign(.center)
.style("boxed-list")
.padding()
}
}
struct Element: Identifiable {
var id: String
}
}
// swiftlint:enable missing_docs

View File

@ -20,6 +20,7 @@ enum Page: String, Identifiable, CaseIterable {
case dice
case overlayWindow
case toast
case list
var id: Self {
self
@ -61,6 +62,8 @@ enum Page: String, Identifiable, CaseIterable {
return "A window on top of another window."
case .toast:
return "Show a notification inside of your app."
case .list:
return "Organize content in multiple rows."
}
}
@ -83,6 +86,8 @@ enum Page: String, Identifiable, CaseIterable {
OverlayWindowDemo(app: app, window: window)
case .toast:
ToastDemo(toast: toast)
case .list:
ListDemo()
}
}