Skip to content

Instantly share code, notes, and snippets.

@mralexhay
Last active March 12, 2022 11:50
Show Gist options
  • Save mralexhay/e795f4ab11c638bf9de564e9dcf8c0e0 to your computer and use it in GitHub Desktop.
Save mralexhay/e795f4ab11c638bf9de564e9dcf8c0e0 to your computer and use it in GitHub Desktop.
Adding items with TextField
//
// ContentView.swift
// TextField
//
// Created by Alex Hay on 11/03/2022.
//
import SwiftUI
/*
This example demonstrates a simple, full SwiftUI example app that lets you add new items to a list.
It handles focus state of the TextFields, shifting focus to new items and moving it to the last item when an item is removed.
It also removes items with blank text when you:
* Press enter/return
* Shift focus to another field
*/
// The basic item containing text and an ID
struct Item: Identifiable {
let id = UUID()
var text: String
}
// Useful for initialising the items with to test long lists
func generateDummyItems() -> [Item] {
var resultArray = [Item]()
for index in 1..<21 {
resultArray.append(Item(text: "Dummy Item \(index)"))
}
return resultArray
}
// Simple view model handling the state of our items, the focus of the current field and any alerts being presented
class ViewModel: ObservableObject {
@Published var focusedItemID: UUID? = nil
@Published var items: [Item] = []
//@Published var items: [Item] = generateDummyItems()
@Published var showingDeleteConfirmation = false
// By default a new item will be added after entering a new item. Change to false to just dismiss the keyboard instead
@Published var addItemOnSubmit = true
func clearFocus() {
focusedItemID = nil
}
func removeItem(_ item: Item) {
items = items.filter({ $0.id != item.id })
}
func remove(at offsets: IndexSet) {
// Swipe to delete at offsets
items.remove(atOffsets: offsets)
}
func addItem(atIndex index: Int? = nil) {
let item = Item(text: "")
if let index = index {
// insert at index if one provided, otherwise add to end of array
items.insert(item, at: index)
} else {
items.append(item)
}
// sets focus to the newly added TextField
focusedItemID = item.id
}
func removeAllItems() {
items = []
focusedItemID = nil
}
func setFocusToLastItem() {
// Set the focused field to the last item's TextField, if it exists
if let lastItemId = items.last?.id {
focusedItemID = lastItemId
} else {
focusedItemID = nil
}
}
}
@main
struct TextFieldApp: App {
@StateObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ItemsListView()
.environmentObject(viewModel)
}
}
}
struct ItemsListView: View {
@EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
ScrollViewReader { scroller in
Form {
ForEach($viewModel.items, id: \.id) { item in
NewItemView(item: item)
.id(item.id)
}
.onDelete { indexSex in
// Swipe to delete item
viewModel.remove(at: indexSex)
}
}
.onChange(of: viewModel.focusedItemID) { newFocusedItemId in
// Automatically scroll the list to the bottom when adding a new item
if let newFocusedItemId = newFocusedItemId {
withAnimation {
scroller.scrollTo(newFocusedItemId, anchor: .top)
}
}
}
}
.navigationTitle("Items")
.toolbar {
// Delete all items
ToolbarItem(placement: .navigationBarLeading) {
// Only show delete button if at least one item is listed
if !viewModel.items.isEmpty {
Button(action: {
viewModel.showingDeleteConfirmation = true
}, label: {
Image(systemName: "trash")
.foregroundColor(.red)
})
}
}
// Add new item nav button
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
viewModel.addItem()
}, label: {
Image(systemName: "plus.circle")
})
}
}
// Alert presented when deleting all items
.alert("Delete All Items?", isPresented: $viewModel.showingDeleteConfirmation) {
Button("Cancel", role: .cancel) { }
Button("Delete", role: .destructive) {
viewModel.removeAllItems()
}
}
}
.navigationViewStyle(.stack) // Simpler view on iPad
}
}
struct NewItemView: View {
@EnvironmentObject var viewModel: ViewModel
@Binding var item: Item
@FocusState private var focusedItemID: UUID?
var body: some View {
TextField("New item", text: $item.text)
.textInputAutocapitalization(.words)
.focused($focusedItemID, equals: item.id)
.onAppear {
// when the TextField appears, if the ID matches the currently focused ID in the view model, make the TextField active
if viewModel.focusedItemID == item.id {
focusedItemID = item.id
}
}
.onChange(of: focusedItemID) { newFocusedItemId in
// if user moves focus to another item when this one is empty, remove it
if newFocusedItemId == nil && item.text.isEmpty {
viewModel.removeItem(item)
}
}
.onChange(of: viewModel.focusedItemID) { newFocusedItemId in
// sync the TextField's focus state to the view model's focused ID
focusedItemID = newFocusedItemId
}
.onChange(of: item.text) { newText in
// if all text is deleted, item is removed from list
if newText.isEmpty {
viewModel.removeItem(item)
viewModel.clearFocus()
}
}
.onSubmit {
// if return is pressed with no text, item is removed otherwise add a new item
if item.text.isEmpty {
viewModel.removeItem(item)
} else {
// Can either add a new item when pressing 'return' or just dismiss the keyboard. Can change preference in the view model
if viewModel.addItemOnSubmit {
if let index = viewModel.items.firstIndex(where: { $0.id == item.id }) {
viewModel.addItem(atIndex: index + 1)
} else {
viewModel.addItem()
}
} else {
viewModel.clearFocus()
}
}
}
}
}
struct ItemsListView_Previews: PreviewProvider {
static var previews: some View {
ItemsListView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment