I`ve adapt official Apple tutorial app for my needs.
There is a ContentListView that displays a list of recipes on one side and a form to edit the selected recipe on the other. The form includes a TextField for editing the recipe's name, bound to a state variable config.recipe.name.
However, there's an issue: when I try to edit the recipe name by typing in the middle of the text (e.g., inserting "Cream" after "Apple" in "Apple Pie"), the cursor unexpectedly jumps to the end of the field as soon as I start typing. This behavior disrupts the editing experience.
import SwiftUI
struct Recipe: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
static var emptyRecipe: Recipe {
Recipe(name: "Empty Recipe")
}
}
struct RecipeEditorConfig {
var recipe = Recipe.emptyRecipe
var originalRecipe = Recipe.emptyRecipe
var shouldSaveChanges = false
var isPresented = false
mutating func presentEditRecipe(_ recipeToEdit: Recipe) {
originalRecipe = recipeToEdit
recipe = recipeToEdit
shouldSaveChanges = false
isPresented = true
}
mutating func done() {
originalRecipe = recipe
shouldSaveChanges = true
isPresented = false
}
var modified: Bool {
return recipe != originalRecipe
}
mutating func save(to recipes: inout [Recipe]) {
if let index = recipes.firstIndex(where: {$0.id == recipe.id}) {
recipes[index] = recipe
}
}
}
struct ContentListView: View {
@State var selectedRecipeID: Recipe.ID?
@State var config: RecipeEditorConfig = RecipeEditorConfig()
@State var listedRecipes: [Recipe] = [
Recipe(name: "Apple Pie"),
Recipe(name: "Vanilla Icecream")
]
var body: some View {
HSplitView {
List(selection: $selectedRecipeID) {
ForEach(listedRecipes) { recipe in
Text(recipe.name)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
ZStack {
if config.isPresented {
Form {
Section("Recipe") {
TextField("Name", text: $config.recipe.name)
}
Button {
config.done()
} label: {
Text("Save")
}
.disabled(!config.modified)
}
.formStyle(.grouped)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
// Save changes
.onChange(of: config.shouldSaveChanges) { shouldSave in
if shouldSave {
config.save(to: &listedRecipes)
}
}
// Show recipe editor for the selected recipe
.onChange(of: selectedRecipeID) { selectedID in
if let id = selectedID {
let recipe = listedRecipes.first(where: {$0.id == id})
config.presentEditRecipe(recipe!)
} else {
config.isPresented = false
}
}
.onAppear {
// If there's no selection but we have recipes, select the first one
if selectedRecipeID == nil && !listedRecipes.isEmpty {
selectedRecipeID = listedRecipes.first?.id
}
}
}
}
#Preview {
ContentListView()
}
Steps to reproduce:
- Click "Apple Pie";
- Try to type something in a middle of the name, for example, add "Cream" after "Apple";
- As you start typing, the cursor suddenly jumps into the end of the field.
I've tried to resolve this issue by taking name variable away from config.recipe and placing it into ContentListView:
import SwiftUI
struct Recipe: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
static var emptyRecipe: Recipe {
Recipe(name: "Empty Recipe")
}
}
struct RecipeEditorConfig {
var recipe = Recipe.emptyRecipe
var originalRecipe = Recipe.emptyRecipe
var shouldSaveChanges = false
var isPresented = false
mutating func presentEditRecipe(_ recipeToEdit: Recipe) {
originalRecipe = recipeToEdit
recipe = recipeToEdit
shouldSaveChanges = false
isPresented = true
}
mutating func done() {
originalRecipe = recipe
shouldSaveChanges = true
isPresented = false
}
var modified: Bool {
return recipe != originalRecipe
}
mutating func save(to recipes: inout [Recipe]) {
if let index = recipes.firstIndex(where: {$0.id == recipe.id}) {
recipes[index] = recipe
}
}
}
struct ContentListView: View {
@State var selectedRecipeID: Recipe.ID?
@State var config: RecipeEditorConfig = RecipeEditorConfig()
@State var listedRecipes: [Recipe] = [
Recipe(name: "Apple Pie"),
Recipe(name: "Vanilla Icecream")
]
@State private var recipeName: String = ""
var body: some View {
HSplitView {
List(selection: $selectedRecipeID) {
ForEach(listedRecipes) { recipe in
Text(recipe.name)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
ZStack {
if config.isPresented {
Form {
Section("Recipe") {
TextField("Name", text: $recipeName)
}
Button {
config.done()
} label: {
Text("Save")
}
.disabled(!config.modified)
}
.formStyle(.grouped)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
// Save changes
.onChange(of: config.shouldSaveChanges) { shouldSave in
if shouldSave {
config.save(to: &listedRecipes)
}
}
.onChange(of: recipeName) { newValue in
config.recipe.name = newValue
}
// Show recipe editor for the selected recipe
.onChange(of: selectedRecipeID) { selectedID in
if let id = selectedID {
let recipe = listedRecipes.first(where: {$0.id == id})
config.presentEditRecipe(recipe!)
recipeName = recipe!.name
} else {
config.isPresented = false
}
}
.onAppear {
// If there's no selection but we have recipes, select the first one
if selectedRecipeID == nil && !listedRecipes.isEmpty {
selectedRecipeID = listedRecipes.first?.id
}
}
}
}
#Preview {
ContentListView()
}
But it doesn't help at all (not to mention it looks terrible).
Why is this bizarre cursor glitch is happening and how to fix it?
mutating func.