diff --git a/Documentation/Reference/README.md b/Documentation/Reference/README.md index 6b75d5d..303940e 100644 --- a/Documentation/Reference/README.md +++ b/Documentation/Reference/README.md @@ -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) diff --git a/Documentation/Reference/classes/State.Content.md b/Documentation/Reference/classes/State.Content.md new file mode 100644 index 0000000..1c0cd02 --- /dev/null +++ b/Documentation/Reference/classes/State.Content.md @@ -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. diff --git a/Documentation/Reference/protocols/StateProtocol.md b/Documentation/Reference/protocols/StateProtocol.md index 3d18b56..be56774 100644 --- a/Documentation/Reference/protocols/StateProtocol.md +++ b/Documentation/Reference/protocols/StateProtocol.md @@ -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. diff --git a/Documentation/Reference/structs/List.md b/Documentation/Reference/structs/List.md index faa8c7d..cf9f791 100644 --- a/Documentation/Reference/structs/List.md +++ b/Documentation/Reference/structs/List.md @@ -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. diff --git a/Documentation/Reference/structs/State.md b/Documentation/Reference/structs/State.md index f5a792c..c111931 100644 --- a/Documentation/Reference/structs/State.md +++ b/Documentation/Reference/structs/State.md @@ -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. diff --git a/Package.swift b/Package.swift index 53a4424..375244e 100644 --- a/Package.swift +++ b/Package.swift @@ -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", diff --git a/README.md b/README.md index daa5828..62c8954 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/Sources/Adwaita/Model/Data Flow/State.swift b/Sources/Adwaita/Model/Data Flow/State.swift index 255f13d..5fcd7da 100644 --- a/Sources/Adwaita/Model/Data Flow/State.swift +++ b/Sources/Adwaita/Model/Data Flow/State.swift @@ -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: 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 { @@ -30,7 +34,8 @@ public struct State: StateProtocol { } /// The stored value. - private let storage: Storage + public let content: State.Content + /// The value with an erased type. public var value: Any { get { @@ -38,7 +43,7 @@ public struct State: StateProtocol { } nonmutating set { if let newValue = newValue as? Value { - storage.value = newValue + content.storage.value = newValue } } } @@ -47,19 +52,33 @@ public struct State: 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 { + 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 } diff --git a/Sources/Adwaita/Model/Data Flow/StateProtocol.swift b/Sources/Adwaita/Model/Data Flow/StateProtocol.swift index c11a74c..f407c6c 100644 --- a/Sources/Adwaita/Model/Data Flow/StateProtocol.swift +++ b/Sources/Adwaita/Model/Data Flow/StateProtocol.swift @@ -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.Content { get } } diff --git a/Sources/Adwaita/View/List.swift b/Sources/Adwaita/View/List.swift index f65d57d..41c4e25 100644 --- a/Sources/Adwaita/View/List.swift +++ b/Sources/Adwaita/View/List.swift @@ -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: 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: 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: 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: 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") diff --git a/Sources/Adwaita/View/StateWrapper.swift b/Sources/Adwaita/View/StateWrapper.swift index 6ac1ee3..aa2350d 100644 --- a/Sources/Adwaita/View/StateWrapper.swift +++ b/Sources/Adwaita/View/StateWrapper.swift @@ -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 { diff --git a/Tests/ListDemo.swift b/Tests/ListDemo.swift new file mode 100644 index 0000000..dc12cc4 --- /dev/null +++ b/Tests/ListDemo.swift @@ -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 diff --git a/Tests/Page.swift b/Tests/Page.swift index 52e15a0..3e8f2ba 100644 --- a/Tests/Page.swift +++ b/Tests/Page.swift @@ -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() } }