List or OutlineGroup expanded by default in SwiftUI - ios

With List or OutlineGroup in SwiftUI how to make some (or all) of their branches expanded by default when creating the view. This seems to be possible with DisclosureGroup with a binding.
This could be useful for restoring state or customizing the view for presentation.

Reusable version of OutlineGroup, where expandability is under control.
import SwiftUI
struct NodeOutlineGroup<Node>: View where Node: Hashable, Node: Identifiable, Node: CustomStringConvertible{
let node: Node
let childKeyPath: KeyPath<Node, [Node]?>
#State var isExpanded: Bool = true
var body: some View {
if node[keyPath: childKeyPath] != nil {
DisclosureGroup(
isExpanded: $isExpanded,
content: {
if isExpanded {
ForEach(node[keyPath: childKeyPath]!) { childNode in
NodeOutlineGroup(node: childNode, childKeyPath: childKeyPath, isExpanded: isExpanded)
}
}
},
label: { Text(node.description) })
} else {
Text(node.description)
}
}
}
struct ContentView: View {
var body: some View {
List {
NodeOutlineGroup(node: data, childKeyPath: \.children, isExpanded: true)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct FileItem: Hashable, Identifiable, CustomStringConvertible {
var id: Self { self }
var name: String
var children: [FileItem]? = nil
var description: String {
switch children {
case nil:
return "📄 \(name)"
case .some(let children):
return children.isEmpty ? "📂 \(name)" : "📁 \(name)"
}
}
}
let data =
FileItem(name: "users", children:
[FileItem(name: "user1234", children:
[FileItem(name: "Photos", children:
[FileItem(name: "photo001.jpg"),
FileItem(name: "photo002.jpg")]),
FileItem(name: "Movies", children:
[FileItem(name: "movie001.mp4")]),
FileItem(name: "Documents", children: [])
]),
FileItem(name: "newuser", children:
[FileItem(name: "Documents", children: [])
])
])
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Very nice, Paul B., thank you.
But I would like to have more control about the displayed rows. So I have expanded your solution a little bit:
struct NodeOutlineGroup<Node, Content>: View where Node: Hashable, Node: Identifiable, Node: CustomStringConvertible, Content: View {
let node: Node
let childKeyPath: KeyPath<Node, [Node]?>
#State var isExpanded: Bool = true
let content: (Node) -> Content
var body: some View {
if node[keyPath: childKeyPath] != nil {
DisclosureGroup(
isExpanded: $isExpanded,
content: {
if isExpanded {
ForEach(node[keyPath: childKeyPath]!) { childNode in
NodeOutlineGroup(node: childNode, childKeyPath: childKeyPath, isExpanded: isExpanded, content: content)
}
}
},
label: { content(node) })
} else {
content(node)
}
}
}
with usage:
struct ContentView: View {
var body: some View {
List {
NodeOutlineGroup(node: data, childKeyPath: \.children, isExpanded: true) { node in
Text(node.description)
}
}
}
}

I was searching for this as well and I believe OutlineGroup doesn't support this. Instead I've moved to DisclosureGroup, which OutlineGroup uses for it's implementation, and directly supports an expansion boolean binding as well as allowing nesting:
struct ToggleStates {
var oneIsOn: Bool = false
var twoIsOn: Bool = true
}
#State private var toggleStates = ToggleStates()
#State private var topExpanded: Bool = true
var body: some View {
DisclosureGroup("Items", isExpanded: $topExpanded) {
Toggle("Toggle 1", isOn: $toggleStates.oneIsOn)
Toggle("Toggle 2", isOn: $toggleStates.twoIsOn)
DisclosureGroup("Sub-items") {
Text("Sub-item 1")
}
}
}
Example from https://developer.apple.com/documentation/swiftui/disclosuregroup

This is nice, but doesn’t allow for selection. Instead, the subtree under each first level item is composited as a single cell, which makes individual selection impossible.

Related

SwiftUI DisclosureGroup Expand each section individually

I'm using a Foreach and a DisclosureGroup to show data.
Each section can Expand/Collapse.
However they all are Expanding/Collapsing at the same time.
How do I Expand/Collapse each section individually?
struct TasksTabView: View {
#State private var expanded: Bool = false
var body: some View {
ForEach(Array(self.dict!.keys.sorted()), id: \.self) { key in
if let tasks = self.dict![key] {
DisclosureGroup(isExpanded: $expanded) {
ForEach(Array(tasks.enumerated()), id:\.1.title) { (index, task) in
VStack(alignment: .leading, spacing: 40) {
PillForRow(index: index, task: task)
.padding(.bottom, 40)
}.onTapGesture {
self.selectedTask = task
}
}
} label: {
Header(title: key, SubtitleText: Text(""), showTag: true, tagValue: tasks.count)
}.accentColor(.rhinoRed)
}
}
}
}
You could have a Set containing the keys of all the expanded sections. If a section is expanded, add it to the set. It is then removed when it is collapsed.
Code:
#State private var expanded: Set<String> = []
DisclosureGroup(
isExpanded: Binding<Bool>(
get: { expanded.contains(key) },
set: { isExpanding in
if isExpanding {
expanded.insert(key)
} else {
expanded.remove(key)
}
}
)
) {
/* ... */
}
It’s not working because the expanded flag links the DiscolureGroup all together. DisclosureGroup is smart enough to expand/collapse each item individually (see below demo).
struct ContentView: View {
struct Task: Identifiable, Hashable {
let id: UUID = UUID()
let name: String = "Task"
}
let allTasks: [[Task]] = [
[Task(), Task()],
[Task()],
[Task(), Task(), Task()]
]
var body: some View {
VStack {
ForEach(allTasks.indices, id: \.self) { indice in
DisclosureGroup() {
ForEach(allTasks[indice]) { task in
Text(task.name)
}
} label: {
Text("Tasks \(indice)")
}
}
}
}
}
However it seems that OutlineGroup is a perfect fit to your use case:
struct Task<Value: Hashable>: Hashable {
let value: Value
var subTasks: [Task]? = nil
}
List(allTasks, id: \.value, children: \.subTasks) { tree in
Text(tree.value).font(.subheadline)
}.listStyle(SidebarListStyle())

SwiftUI: Sheet cannot show correct values in first time

I found strange behavior in SwiftUI.
The sheet shows empty text when I tap a list column first time.
It seems correct after second time.
Would you help me?
import SwiftUI
let fruits: [String] = [
"Apple",
"Banana",
"Orange",
]
struct ContentView: View {
#State var isShowintSheet = false
#State var selected: String = ""
var body: some View {
NavigationView {
List(fruits, id: \.self) { fruit in
Button(action: {
selected = fruit
isShowintSheet = true
}) {
Text(fruit)
}
}
}
.sheet(isPresented: $isShowintSheet, content: {
Text(selected)
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
list
first tap
after second tap
Use .sheet(item:) instead. Here is fixed code.
Verified with Xcode 12.1 / iOS 14.1
struct ContentView: View {
#State var selected: String?
var body: some View {
NavigationView {
List(fruits, id: \.self) { fruit in
Button(action: {
selected = fruit
}) {
Text(fruit)
}
}
}
.sheet(item: $selected, content: { item in
Text(item)
})
}
}
extension String: Identifiable {
public var id: String { self }
}
Thank you, Omid.
I changed my code from Asperi's code using #State like this.
import SwiftUI
struct Fruit: Identifiable, Hashable {
var name: String
var id = UUID()
}
let fruits: [Fruit] = [
Fruit(name: "Apple"),
Fruit(name: "Banana"),
Fruit(name: "Orange"),
]
struct ContentView: View {
#State var selected: Fruit?
var body: some View {
NavigationView {
List(fruits, id: \.self) { fruit in
Button(action: {
selected = fruit
}) {
Text(fruit.name)
}
}
}
.sheet(item: $selected, content: { item in
Text(item.name)
})
}
}

How can I fix "index out of Range" for a multi-dimensional view in SwiftUI

I tried as much as I could before asking the next "Index out of Range" question, because generally I understand why an index out of range issue occurs, but this specific issues makes me crazy:
struct Parent: Identifiable {
let id = UUID()
let name: String
var children: [Child]?
}
struct Child: Identifiable {
let id = UUID()
let name: String
var puppets: [Puppet]?
}
struct Puppet: Identifiable {
let id = UUID()
let name: String
}
class AppState: ObservableObject {
#Published var parents: [Parent]
init() {
self.parents = [
Parent(name: "Foo", children: [Child(name: "bar", puppets: [Puppet(name: "Tom")])]),
Parent(name: "FooBar", children: [Child(name: "foo", puppets: nil)]),
Parent(name: "FooBar", children: nil)
]
}
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
List {
ForEach (appState.parents.indices, id: \.self) { parentIndex in
NavigationLink (destination: ChildrenView(parentIndex: parentIndex).environmentObject(self.appState)) {
Text(self.appState.parents[parentIndex].name)
}
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.appState.parents.append(Parent(name: "Test", children: nil))
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Parents"))
}
}
private func deleteItem(at indexSet: IndexSet) {
self.appState.parents.remove(atOffsets: indexSet)
}
}
struct ChildrenView: View {
#EnvironmentObject var appState: AppState
var parentIndex: Int
var body: some View {
let children = appState.parents[parentIndex].children
return VStack {
List {
if (children?.indices != nil) {
ForEach (children!.indices, id: \.self) { childIndex in
NavigationLink (destination: PuppetsView(parentIndex: self.parentIndex, childIndex: childIndex).environmentObject(self.appState)) {
Text(children![childIndex].name)
}
}
.onDelete(perform: deleteItem)
}
}
Button(action: {
var children = self.appState.parents[self.parentIndex].children
if (children != nil) {
children?.append(Child(name: "Teest"))
} else {
children = [Child(name: "Teest")]
}
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Children"))
}
private func deleteItem(at indexSet: IndexSet) {
if (self.appState.parents[self.parentIndex].children != nil) {
self.appState.parents[self.parentIndex].children!.remove(atOffsets: indexSet)
}
}
}
struct PuppetsView: View {
#EnvironmentObject var appState: AppState
var parentIndex: Int
var childIndex: Int
var body: some View {
let child = appState.parents[parentIndex].children?[childIndex]
return VStack {
List {
if (child != nil && child!.puppets?.indices != nil) {
ForEach (child!.puppets!.indices, id: \.self) { puppetIndex in
Text(self.appState.parents[self.parentIndex].children![self.childIndex].puppets![puppetIndex].name)
}
.onDelete(perform: deleteItem)
}
}
Button(action: {
var puppets = self.appState.parents[self.parentIndex].children![self.childIndex].puppets
if (puppets != nil) {
puppets!.append(Puppet(name: "Teest"))
} else {
puppets = [Puppet(name: "Teest")]
}
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Puppets"))
}
private func deleteItem(at indexSet: IndexSet) {
if (self.appState.parents[self.parentIndex].children != nil) {
self.appState.parents[self.parentIndex].children![self.childIndex].puppets!.remove(atOffsets: indexSet)
}
}
}
I can remove both children of Foo and FooBar without issues, but when I remove the Puppet of child bar first, then the app crashes like shown in the comments.
I unterstand that the childIndex doesn't exist anymore, but I don't understand why the view gets built again when there is no child with puppets.
All the referencing of array indices looks pretty awful to me. Using array indices also requires that you pass the various objects down to the subviews.
To address this I started by changing your models - Make them classes rather than structs so you can make them #ObservableObject. They also need to be Hashable and Equatable.
I also added add and remove functions to the model objects so that you don't need to worry about indices when adding/removing children/puppets. The remove methods use an array extension that removes an Identifiable object without needing to know the index.
Finally, I changed the children and puppets arrays to be non-optional. There is little semantic difference between a nil optional and an empty non-optional array, but the latter is much easier to deal with.
class Parent: ObservableObject, Hashable {
static func == (lhs: Parent, rhs: Parent) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
let id = UUID()
let name: String
#Published var children: [Child]
init(name: String, children: [Child]? = nil) {
self.name = name
self.children = children ?? []
}
func remove(child: Child) {
self.children.remove(child)
}
func add(child: Child) {
self.children.append(child)
}
}
class Child: ObservableObject, Identifiable, Hashable {
static func == (lhs: Child, rhs: Child) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
let id = UUID()
let name: String
#Published var puppets: [Puppet]
init(name: String, puppets:[Puppet]? = nil) {
self.name = name
self.puppets = puppets ?? []
}
func remove(puppet: Puppet) {
self.puppets.remove(puppet)
}
func add(puppet: Puppet) {
self.puppets.append(puppet)
}
}
struct Puppet: Identifiable, Hashable {
let id = UUID()
let name: String
}
class AppState: ObservableObject {
#Published var parents: [Parent]
init() {
self.parents = [
Parent(name: "Foo", children: [Child(name: "bar", puppets: [Puppet(name: "Tom")])]),
Parent(name: "FooBar", children: [Child(name: "foo", puppets: nil)])
]
}
}
extension Array where Element: Identifiable {
mutating func remove(_ object: Element) {
if let index = self.firstIndex(where: { $0.id == object.id}) {
self.remove(at: index)
}
}
}
Having sorted out the model, the views then only need to know about their specific item:
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
List {
ForEach (appState.parents, id: \.self) { parent in
NavigationLink (destination: ChildrenView(parent: parent)) {
Text(parent.name)
}
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.appState.parents.append(Parent(name: "Test", children: nil))
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Parents"))
}
}
private func deleteItem(at indexSet: IndexSet) {
self.appState.parents.remove(atOffsets: indexSet)
}
}
struct ChildrenView: View {
#ObservedObject var parent: Parent
var body: some View {
VStack {
List {
ForEach (self.parent.children, id: \.self) { child in
NavigationLink (destination: PuppetsView(child:child)) {
Text(child.name)
}
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.parent.add(child: Child(name: "Test"))
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Children"))
}
private func deleteItem(at indexSet: IndexSet) {
let children = Array(indexSet).map { self.parent.children[$0]}
for child in children {
self.parent.remove(child: child)
}
}
}
struct PuppetsView: View {
#ObservedObject var child: Child
var body: some View {
VStack {
List {
ForEach (child.puppets, id: \.self) { puppet in
Text(puppet.name)
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.child.add(puppet:Puppet(name: "Test"))
})
{
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Puppets"))
}
func deleteItem(at indexSet: IndexSet) {
let puppets = Array(indexSet).map { self.child.puppets[$0] }
for puppet in puppets {
self.child.remove(puppet:puppet)
}
}
}
The problem with your optional chaining is that this line produces the result of type Child and not Child?:
appState.parents[parentIndex].children?[childIndex]
And if it's not an optional you can't call puppets on children?[childIndex] without checking if childIndex is valid:
// this will crash when childIndex is out of range
appState.parents[parentIndex].children?[childIndex].puppets?.indices
I recommend to use safeIndex subscript for accessing possible empty elements:
var body: some View {
let child = appState.parents[safeIndex: parentIndex]?.children?[safeIndex: childIndex]
return VStack {
List {
if (child != nil && child!.puppets?.indices != nil) {
ForEach ((appState.parents[parentIndex].children?[childIndex].puppets!.indices)!, id: \.self) { puppetIndex in
Text(self.appState.parents[self.parentIndex].children![self.childIndex].puppets![puppetIndex].name)
}
.onDelete(perform: deleteItem)
}
}
...
}
To do this you would need an Array extension which allows to access array elements in a safe way (ie. return nil instead of throwing an error):
extension Array {
public subscript(safeIndex index: Int) -> Element? {
guard index >= 0, index < endIndex else {
return nil
}
return self[index]
}
}
Note: You'd need to do the same for the ParentView, so in overall Paulw11's answer is cleaner.

SwiftUI: How to update passing array item in the other view

I'm trying to update arrays item with typed new value into Textfield, but List is not updated with edited value.
My Code is:
Model:
struct WalletItem: Identifiable{
let id = UUID()
var name:String
var cardNumber:String
var type:String
var cvc:String
let pin:String
var dateOfExpiry:String
}
ModelView:
class Wallet: ObservableObject{
#Published var wallets = [
WalletItem(name: "BSB", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2016-06-29"),
WalletItem(name: "Alpha bank", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2017-03-12"),
WalletItem(name: "MTБ", cardNumber: "123456789", type: "master card", cvc: "1234", pin: "1234", dateOfExpiry: "2020-11-12"),
]
}
First View:
struct WalletListView: View {
// Properties
// ==========
#ObservedObject var wallet = Wallet()
#State var isNewItemSheetIsVisible = false
var body: some View {
NavigationView {
List(wallet.wallets) { walletItem in
NavigationLink(destination: EditWalletItem(walletItem: walletItem)){
Text(walletItem.name)
}
}
.navigationBarTitle("Cards", displayMode: .inline)
.navigationBarItems(
leading: Button(action: { self.isNewItemSheetIsVisible = true
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add item")
}
}
)
}
.sheet(isPresented: $isNewItemSheetIsVisible) {
NewWalletItem(wallet: self.wallet)
}
}
}
and Secondary View:
struct EditWalletItem: View {
#State var walletItem: WalletItem
#Environment(\.presentationMode) var presentationMode
var body: some View {
Form{
Section(header: Text("Card Name")){
TextField("", text: $walletItem.name)
}
}
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Back")
}, trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
})
{
Text("Save")
})
}
}
P.S: If I use #Binding instead of the #State I've got an error in the first view: Initializer init(_:) requires that Binding<String> conform to StringProtocol
Here are modified parts (tested & works with Xcode 11.2 / iOS 13.2):
Sure over binding
struct EditWalletItem: View {
#Binding var walletItem: WalletItem
Place to pass it
List(Array(wallet.wallets.enumerated()), id: .element.id) { (i, walletItem) in
NavigationLink(destination: EditWalletItem(walletItem: self.$wallet.wallets[i])){
Text(walletItem.name)
}
}
ForEach(Array(list.enumerated())) will only work correctly if the list is an Array but not for an ArraySlice, and it has the downside of copying the list.
A better approach is using a .indexed() helper:
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { self.base.startIndex }
var endIndex: Index { self.base.endIndex }
func index(after i: Index) -> Index {
self.base.index(after: i)
}
func index(before i: Index) -> Index {
self.base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
self.base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: self.base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
Example:
// SwiftUIPlayground
// https://github.com/ralfebert/SwiftUIPlayground/
import Foundation
import SwiftUI
struct Position {
var id = UUID()
var count: Int
var name: String
}
class BookingModel: ObservableObject {
#Published var positions: [Position]
init(positions: [Position] = []) {
self.positions = positions
}
}
struct EditableListExample: View {
#ObservedObject var bookingModel = BookingModel(
positions: [
Position(count: 1, name: "Candy"),
Position(count: 0, name: "Bread"),
]
)
var body: some View {
// >>> Passing a binding into an Array via index:
List(bookingModel.positions.indexed(), id: \.element.id) { i, _ in
PositionRowView(position: self.$bookingModel.positions[i])
}
}
}
struct PositionRowView: View {
#Binding var position: Position
var body: some View {
Stepper(
value: $position.count,
label: {
Text("\(position.count)x \(position.name)")
}
)
}
}
struct EditableListExample_Previews: PreviewProvider {
static var previews: some View {
EditableListExample()
}
}
See also:
How does the Apple-suggested .indexed() property work in a ForEach?

SwiftUI Textfields inside Lists

I want a list with rows, with each row having 2 Textfields inside of it. Those rows should be saved in an array, so that I can use the data in an other view for further functions. If the text in the Textfield is changed, the text should be saved inside the right entry in the array.
You also can add new rows to the list via a button, which should also change the array for the rows.
The goal is to have a list of key value pairs, each one editable and those entries getting saved with the current text.
Could someone help me and/or give me hint for fixing this problem?
So far I have tried something like this:
// the array of list entries
#State var list: [KeyValue] = [KeyValue()]
// the List inside of a VStack
List(list) { entry in
KeyValueRow(list.$key, list.$value)
}
// the single item
struct KeyValue: Identifiable {
var id = UUID()
#State var key = ""
#State var value = ""
}
// one row in the list with view elements
struct KeyValueRow: View {
var keyBinding: Binding<String>
var valueBinding: Binding<String>
init(_ keyBinding: Binding<String>, _ valueBinding: Binding<String>){
self.keyBinding = keyBinding
self.valueBinding = valueBinding
}
var body: some View {
HStack() {
TextField("key", text: keyBinding)
Spacer()
TextField("value", text: valueBinding)
Spacer()
}
}
}
Also, about the button for adding new entries.
Problem is that if I do the following, my list in the view goes blank and everything is empty again
(maybe related: SwiftUI TextField inside ListView goes blank after filtering list items ?)
Button("Add", action: {
self.list.append(KeyValue())
})
I am not sure what the best practice is keep a view up to date with state in an array like this, but here is one approach to make it work.
For the models, I added a list class that conforms to Observable object, and each KeyValue item alerts it on changes:
class KeyValueList: ObservableObject {
#Published var items = [KeyValue]()
func update() {
self.objectWillChange.send()
}
func addItem() {
self.items.append(KeyValue(parent: self))
}
}
class KeyValue: Identifiable {
init(parent: KeyValueList) {
self.parent = parent
}
let id = UUID()
private let parent: KeyValueList
var key = "" {
didSet { self.parent.update() }
}
var value = "" {
didSet { self.parent.update() }
}
}
Then I was able to simply the row view to just keep a single piece of state:
struct KeyValueRow: View {
#State var item: KeyValue
var body: some View {
HStack() {
TextField("key", text: $item.key)
Spacer()
TextField("value", text: $item.value)
Spacer()
}
}
}
And for the list view:
struct TextFieldList: View {
#ObservedObject var list = KeyValueList()
var body: some View {
VStack {
List(list.items) { item in
HStack {
KeyValueRow(item: item)
Text(item.key)
}
}
Button("Add", action: {
self.list.addItem()
})
}
}
}
I just threw an extra Text in there for testing to see it update live.
I did not run into the Add button blanking the view as you described. Does this solve that issue for you as well?
Working code example for iOS 15
In SwiftUI, Apple recommends passing the binding directly into the List constructor and using a #Binding in the ViewBuilder block to iterate through with each element.
Apple recommends this approach over using the Indices to iterate over the collection since this doesn't reload the whole list every time a TextField value changes (better efficiency).
The new syntax is also back-deployable to previous releases of SwiftUI apps.
struct ContentView: View {
#State var directions: [Direction] = [
Direction(symbol: "car", color: .mint, text: "Drive to SFO"),
Direction(symbol: "airplane", color: .blue, text: "Fly to SJC"),
Direction(symbol: "tram", color: .purple, text: "Ride to Cupertino"),
Direction(symbol: "bicycle", color: .orange, text: "Bike to Apple Park"),
Direction(symbol: "figure.walk", color: .green, text: "Walk to pond"),
Direction(symbol: "lifepreserver", color: .blue, text: "Swim to the center"),
Direction(symbol: "drop", color: .indigo, text: "Dive to secret airlock"),
Direction(symbol: "tram.tunnel.fill", color: .brown, text: "Ride through underground tunnels"),
Direction(symbol: "key", color: .red, text: "Enter door code:"),
]
var body: some View {
NavigationView {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
}
}
.listStyle(.sidebar)
.navigationTitle("Secret Hideout")
}
}
}
struct Direction: Identifiable {
var id = UUID()
var symbol: String
var color: Color
var text: String
}
No need to mess up with classes, Observable, Identifiable. You can do it all with structs.
Note, that version below will do fine for insertions, but fail if you try to delete array elements:
import SwiftUI
// the single item
struct KeyValue {
var key: String
var value: String
}
struct ContentView: View {
#State var boolArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]
var body: some View {
NavigationView {
// id: \.self is obligatory if you need to insert
List(boolArr.indices, id: \.self) { idx in
HStack() {
TextField("key", text: self.$boolArr[idx].key)
Spacer()
TextField("value", text: self.$boolArr[idx].value)
Spacer()
}
}
.navigationBarItems(leading:
Button(action: {
self.boolArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
print(self.boolArr)
})
{ Text("Add") }
, trailing:
Button(action: {
self.boolArr.removeLast() // causes "Index out of range" error
print(self.boolArr)
})
{ Text("Remove") })
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Update:
A little trick to make it work with deletions as well.
import SwiftUI
// the single item
struct KeyValue {
var key: String
var value: String
}
struct KeyValueView: View {
#Binding var model: KeyValue
var body: some View {
HStack() {
TextField("Key", text: $model.key)
Spacer()
TextField("Value", text: $model.value)
Spacer()
}
}
}
struct ContentView: View {
#State var kvArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]
var body: some View {
NavigationView {
List(kvArr.indices, id: \.self) { i in
KeyValueView(model: Binding(
get: {
return self.kvArr[i]
},
set: { (newValue) in
self.kvArr[i] = newValue
}))
}
.navigationBarItems(leading:
Button(action: {
self.kvArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
print(self.kvArr)
})
{ Text("Add") }
, trailing:
Button(action: {
self.kvArr.removeLast() // Works like a charm
print(self.kvArr)
})
{ Text("Remove") })
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Swift 5.5
This version of swift enables one line code for this by passing the bindable item directly from the array.
struct DirectionsList: View {
#Binding var directions: [Direction]
var body: some View {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
} icon: {
DirectionsIcon(direction)
}
}
}
}

Resources