From 0d764502f7dab65164b529aa0b14030c45ac3042 Mon Sep 17 00:00:00 2001 From: Qijia Liu Date: Fri, 13 Dec 2024 22:36:52 -0500 Subject: [PATCH] ListView; fallback Key to String (#33) --- src/config/BooleanView.swift | 2 +- src/config/EnumView.swift | 2 +- src/config/ListView.swift | 81 ++++++++++++++++++++++++++++++++++++ src/config/StringView.swift | 6 ++- src/config/option.swift | 5 ++- 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 src/config/ListView.swift diff --git a/src/config/BooleanView.swift b/src/config/BooleanView.swift index 89ccc42..ed90b0a 100644 --- a/src/config/BooleanView.swift +++ b/src/config/BooleanView.swift @@ -9,7 +9,7 @@ struct BooleanView: OptionViewProtocol { Toggle( isOn: Binding( get: { value as! String == "True" }, - set: { x in value = x ? "True" : "False" } + set: { value = $0 ? "True" : "False" } ) ) { Text(label) diff --git a/src/config/EnumView.swift b/src/config/EnumView.swift index 1d66399..e8eaeaf 100644 --- a/src/config/EnumView.swift +++ b/src/config/EnumView.swift @@ -18,7 +18,7 @@ struct EnumView: OptionViewProtocol { label, selection: Binding( get: { value as! String }, - set: { x in value = x } + set: { value = $0 } ) ) { ForEach(dataToOptions(data), id: \.0) { pair in diff --git a/src/config/ListView.swift b/src/config/ListView.swift new file mode 100644 index 0000000..287946f --- /dev/null +++ b/src/config/ListView.swift @@ -0,0 +1,81 @@ +import SwiftUI + +private func deserialize(_ value: Any) -> [Any] { + guard let value = value as? [String: Any] else { + return [] + } + return (0.. [String: Any] { + return value.enumerated().reduce(into: [String: Any]()) { result, pair in + result[String(pair.offset)] = pair.element + } +} + +private struct ListSectionHeader: View { + @Binding var value: Any + + var body: some View { + HStack { + Spacer() + Button { + var list = deserialize(value) + list.append("") + value = serialize(list) + } label: { + Image(systemName: "plus") + } + } + } +} + +struct ListSubView: OptionViewProtocol { + let label: String + let data: [String: Any] + @Binding var value: Any + + var body: some View { + let type = data["Type"] as! String + let optionViewType = toOptionViewType(["Type": String(type.suffix(type.count - "List|".count))]) + var list = deserialize(value) + List { + Section(header: ListSectionHeader(value: $value)) { + ForEach(list.indices, id: \.self) { i in + AnyView( + optionViewType.init( + label: "", data: [:], + value: Binding( + get: { list[i] }, + set: { + list[i] = $0 + value = serialize(list) + } + ))) + } + .onDelete { offsets in + list.remove(atOffsets: offsets) + value = serialize(list) + } + .onMove { indices, newOffset in + list.move(fromOffsets: indices, toOffset: newOffset) + value = serialize(list) + } + } + }.navigationTitle(label) + } +} + +struct ListView: OptionViewProtocol { + let label: String + let data: [String: Any] + @Binding var value: Any + + var body: some View { + NavigationLink( + destination: ListSubView(label: label, data: data, value: $value) + ) { + Text(label) + } + } +} diff --git a/src/config/StringView.swift b/src/config/StringView.swift index ad572e5..2e9aac1 100644 --- a/src/config/StringView.swift +++ b/src/config/StringView.swift @@ -14,9 +14,11 @@ struct StringView: View, OptionViewProtocol { "", text: Binding( get: { value as! String }, - set: { x in value = x } + set: { value = $0 } ) - ).multilineTextAlignment(.trailing) + ) + // Leading for List item, trailing for String option. + .multilineTextAlignment(label.isEmpty ? .leading : .trailing) } } } diff --git a/src/config/option.swift b/src/config/option.swift index 689cad5..4d238e5 100644 --- a/src/config/option.swift +++ b/src/config/option.swift @@ -73,7 +73,7 @@ func toOptionViewType(_ data: [String: Any]) -> any OptionViewProtocol.Type { return EnumView.self case "Integer": return IntegerView.self - case "String": + case "String", "Key": if data["IsEnum"] as? String == "True" { return EnumView.self } @@ -81,6 +81,9 @@ func toOptionViewType(_ data: [String: Any]) -> any OptionViewProtocol.Type { case "External": return ExternalView.self default: + if type.starts(with: "List|") { + return ListView.self + } return UnknownView.self } }