Last active
March 12, 2022 11:50
-
-
Save mralexhay/e795f4ab11c638bf9de564e9dcf8c0e0 to your computer and use it in GitHub Desktop.
Adding items with TextField
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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