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:
parent
37252f7b8f
commit
1c50b3b923
@ -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)
|
||||
|
||||
16
Documentation/Reference/classes/State.Content.md
Normal file
16
Documentation/Reference/classes/State.Content.md
Normal 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.
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 }
|
||||
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
56
Tests/ListDemo.swift
Normal 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
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user