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 ## Classes
- [GTUIApp](classes/GTUIApp.md) - [GTUIApp](classes/GTUIApp.md)
- [State.Content](classes/State.Content.md)
- [State.Storage](classes/State.Storage.md) - [State.Storage](classes/State.Storage.md)
- [ViewStorage](classes/ViewStorage.md) - [ViewStorage](classes/ViewStorage.md)
- [WindowStorage](classes/WindowStorage.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. An interface for accessing `State` without specifying the generic type.
## Properties ## 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. The identifier of the selected element.
### `elementsID`
The identifier of the elements storage.
## Methods ## Methods
### `init(_:selection:content:)` ### `init(_:selection:content:)`
@ -39,11 +43,27 @@ Get a view storage.
- Parameter modifiers: Modify views before being updated. - Parameter modifiers: Modify views before being updated.
- Returns: The view storage. - 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:)` ### `updateSelection(box:)`
Update the list's selection. Update the list's selection.
- Parameter box: The list box. - 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()` ### `sidebarStyle()`
Add the "navigation-sidebar" style class. 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. Get the value as a binding using the `$` prefix.
### `storage` ### `content`
The stored value. The stored value.

View File

@ -18,12 +18,19 @@ let package = Package(
) )
], ],
dependencies: [ 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: [ targets: [
.target( .target(
name: "Adwaita", name: "Adwaita",
dependencies: [.product(name: "Libadwaita", package: "Libadwaita")] dependencies: [
.product(name: "Libadwaita", package: "Libadwaita"),
.product(name: "LevenshteinTransformations", package: "LevenshteinTransformations")
]
), ),
.executableTarget( .executableTarget(
name: "Swift Adwaita Demo", name: "Swift Adwaita Demo",

View File

@ -115,6 +115,7 @@ I recommend using the [template repository](https://github.com/AparokshaUI/Adwai
### Dependencies ### Dependencies
- [Libadwaita][18] licensed under the [GPL-3.0 license][19] - [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 ### Other Thanks
- The [contributors][20] - The [contributors][20]

View File

@ -5,20 +5,24 @@
// Created by david-swift on 06.08.23. // 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. /// 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 {
// swiftlint:disable force_cast
/// 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 {
get { get {
storage.value content.storage.value as! Value
} }
nonmutating set { nonmutating set {
storage.value = newValue content.storage.value = newValue
Self.updateViews() Self.updateViews()
} }
} }
// swiftlint:enable force_cast
/// Get the value as a binding using the `$` prefix. /// Get the value as a binding using the `$` prefix.
public var projectedValue: Binding<Value> { public var projectedValue: Binding<Value> {
@ -30,7 +34,8 @@ public struct State<Value>: StateProtocol {
} }
/// The stored value. /// The stored value.
private let storage: Storage<Value> public let content: State<Any>.Content
/// The value with an erased type. /// The value with an erased type.
public var value: Any { public var value: Any {
get { get {
@ -38,7 +43,7 @@ public struct State<Value>: StateProtocol {
} }
nonmutating set { nonmutating set {
if let newValue = newValue as? Value { if let newValue = newValue as? Value {
storage.value = newValue content.storage.value = newValue
} }
} }
} }
@ -47,19 +52,33 @@ public struct State<Value>: StateProtocol {
/// - Parameters: /// - Parameters:
/// - wrappedValue: The wrapped value. /// - wrappedValue: The wrapped value.
public init(wrappedValue: 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. /// A class storing the value.
class Storage<StoredValue> { public class Storage {
/// The stored value. /// The stored value.
var value: StoredValue public var value: Any
/// Initialize the storage. /// Initialize the storage.
/// - Parameters: /// - Parameters:
/// - value: The value. /// - value: The value.
init(value: StoredValue) { public init(value: Any) {
self.value = value self.value = value
} }

View File

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

View File

@ -5,6 +5,7 @@
// Created by david-swift on 25.09.23. // Created by david-swift on 25.09.23.
// //
import LevenshteinTransformations
import Libadwaita import Libadwaita
/// A list box widget. /// A list box widget.
@ -17,6 +18,9 @@ public struct List<Element>: Widget where Element: Identifiable {
/// The identifier of the selected element. /// The identifier of the selected element.
@Binding var selection: Element.ID @Binding var selection: Element.ID
/// The identifier of the elements storage.
let elementsID = "elements"
/// Initialize `List`. /// Initialize `List`.
/// - Parameters: /// - Parameters:
/// - elements: The elements. /// - elements: The elements.
@ -38,7 +42,12 @@ public struct List<Element>: Widget where Element: Identifiable {
/// - modifiers: Modify views before being updated. /// - modifiers: Modify views before being updated.
public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) {
if let box = storage.view as? ListBox { 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 { public func container(modifiers: [(View) -> View]) -> ViewStorage {
let box: ListBox = .init() let box: ListBox = .init()
var content: [ViewStorage] = [] var content: [ViewStorage] = []
for element in elements { updateList(box: box, content: .init { content } set: { content = $0 }, modifiers: modifiers)
let widget = self.content(element).widget(modifiers: modifiers).container(modifiers: modifiers)
_ = box.append(widget.view)
content.append(widget)
}
_ = box.handler { _ = box.handler {
let selection = box.getSelectedRow() 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 self.selection = id
} }
} }
updateSelection(box: box)
return .init(box, content: [.mainContent: content]) 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. /// Update the list's selection.
/// - Parameter box: The list box. /// - Parameter box: The list box.
func updateSelection(box: ListBox) { 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. /// Add the "navigation-sidebar" style class.
public func sidebarStyle() -> View { public func sidebarStyle() -> View {
style("navigation-sidebar") style("navigation-sidebar")

View File

@ -36,8 +36,8 @@ public struct StateWrapper: Widget {
/// - modifiers: Modify views before being updated. /// - modifiers: Modify views before being updated.
public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) {
for property in state { for property in state {
if let value = storage.state[property.key]?.value { if let storage = storage.state[property.key]?.content.storage {
property.value.value = value property.value.content.storage = storage
} }
} }
if let storage = storage.content[.mainContent]?.first { 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 dice
case overlayWindow case overlayWindow
case toast case toast
case list
var id: Self { var id: Self {
self self
@ -61,6 +62,8 @@ enum Page: String, Identifiable, CaseIterable {
return "A window on top of another window." return "A window on top of another window."
case .toast: case .toast:
return "Show a notification inside of your app." 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) OverlayWindowDemo(app: app, window: window)
case .toast: case .toast:
ToastDemo(toast: toast) ToastDemo(toast: toast)
case .list:
ListDemo()
} }
} }