forked from aparoksha/adwaita-swift
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
|
## 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)
|
||||||
|
|||||||
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.
|
An interface for accessing `State` without specifying the generic type.
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
### `value`
|
### `content`
|
||||||
|
|
||||||
The type-erased value.
|
The class storing the value.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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
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 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user