Dynamic List of items in SwiftUI

I have found that it is not uncommon in SwiftUI to end up with a Binding<Array<Item>> datatype. Handling this in a nice way to enable seamless editing of the data is not always that straight forward.

In this post I will share my solution for handling an array of strings to produce a list of text fields. You can dynamically add additional text fields and support swipe to delete.

Firstly, let us consider our content view:

struct ContentView: View {
    @Binding var items: [String]
    
    var body: some View {
        Form {
            ForEach(0..<items.count, id: \.self) { index in
                TextField(
                    "Field \(index)",
                    text: $items[index]
                )
            }
        }
    }
}

This content view will render a list of TextFields with each row bound to the corresponding item in the items array. But it does not include adding additional rows, so the user is unable to add any new items for now.

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Level up your Swift skills!$35

100+ tips to take your Swift code to the next level

Swift Gemsby Natalia Panferova

  • Advanced Swift techniques for experienced developers bypassing basic tutorials
  • Curated, actionable tips ready for immediate integration into any Swift project
  • Strategies to improve code quality, structure, and performance across all platforms

Level up your Swift skills!

100+ tips to take your Swift code to the next level

Swift Gems by Natalia Panferova book coverSwift Gems by Natalia Panferova book cover

Swift Gems

by Natalia Panferova

$35

# Adding swipe to delete

Both List and Form views in SwiftUI support a simple operator that enables swipe to delete actions. This is onDelete() modifier that can be placed on ForEach view.

var body: some View {
    Form {
        ForEach(0..<items.count, id: \.self) { index in
            TextField(
                "Field \(index)",
                text: $items[index]
            )
        }
        .onDelete(perform: delete)
    }
}

func delete(_ indexSet: IndexSet) {
    items.remove(atOffsets: indexSet)
}

Notice the new delete() method we have defined that uses remove(atOffsets:) method on arrays to remove multiple items at once.

# Adding an empty new item row

To make this list fully editable I would like to add an empty row to the bottom of the list so that when the user starts to type the content of this row is dynamically added to our items array. It is important that the view hierarchy does not change while the user is typing otherwise the user might lose keyboard focus.

To do this I will add an extension to Binding so that when indexing a row that is out of range, rather than crashing the application, a default value is returned.

extension Binding where
    Value: MutableCollection,
    Value: RangeReplaceableCollection
{
    subscript(
        _ index: Value.Index,
        default defaultValue: Value.Element
    ) -> Binding<Value.Element> {
        Binding<Value.Element> {
            guard index < self.wrappedValue.endIndex else {
                return defaultValue
            }
            return self.wrappedValue[index]
        } set: { newValue in
            // It is possible that the index we are updating
            // is beyond the end of our array so we first
            // need to append items to the array to ensure
            // we are within range.
            while index >= self.wrappedValue.endIndex {
                self.wrappedValue.append(defaultValue)
            }
            
            self.wrappedValue[index] = newValue
        }
    }
}

Using this extension within our view body is simple:

var body: some View {
    Form {
        ForEach(0..<items.count + 1, id: \.self) { index in
            TextField(
                "Field \(index)",
                text: $items[index, default: ""]
            )
        }
        .onDelete(perform: self.delete)
    }
}

This ForEach adds an extra iteration that goes past the end of our items list. As soon as a user starts to type the Binding returned by the above extension will append a new row to our items list and ForEach will then add an additional empty row for the next item the user wants to add.

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Enhance older apps with SwiftUI!$45

A detailed guide on gradually adopting SwiftUI in UIKit projects

Updated for iOS 18 and Xcode 16!

Integrating SwiftUI into UIKit Appsby Natalia Panferova

  • Upgrade your apps with new features like Swift Charts and Widgets
  • Support older iOS versions with effective backward-compatible strategies
  • Seamlessly bridge state and data between UIKit and SwiftUI using the latest APIs

Enhance older apps with SwiftUI!

A detailed guide on gradually adopting SwiftUI in UIKit projects

Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Integrating SwiftUI
into UIKit Apps

by Natalia Panferova

Updated for iOS 18 and Xcode 16!

$45