0

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:

  1. Click "Apple Pie";
  2. Try to type something in a middle of the name, for example, add "Cream" after "Apple";
  3. 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?

3
  • Could not replicate your problem, all works well for me, typing in the middle of the name as per your description, does not make the cursor jump to the end of the field. On macOS Sequoia 15.5 using Xcode 16.3. Note, I think you are forcing old-style procedural programming with SwiftUI declarative based syntax. Restructure your code to remove those mutating func. Commented Apr 11 at 23:18
  • I'm using Xcode 15.2 on macOS Ventura 13.7.5. Not sure is it "forcing old-style procedural programming", since it is published by Apple. Are you sure that Apple itself will force old-style programming in an official tutorial? I've modified it a bit, but the idea stays the same. Commented Apr 12 at 6:42
  • yes, some Apple examples sometimes do not compile or run, or both. Note if it was some kind of bug on older macOS, then it is fixed in macOS Sequoia 15.5 using Xcode 16.3. Commented Apr 12 at 6:59

1 Answer 1

0

Try this alternative approach using the declarative syntax aspect of SwiftUI with HSplitView, to achieve consistent results.

Example code:


struct ContentView: View {
    var body: some View {
        ContentListView()
    }
}

struct Recipe: Identifiable, Hashable {
    let id: UUID = UUID() // <--- here
    var name: String
}

struct ContentListView: View {
    @State private var selectedRecipe = Recipe(name: "Empty Recipe")  // <--- here
    @State private var listedRecipes = [
        Recipe(name: "Apple Pie"),
        Recipe(name: "Vanilla Icecream")
    ]

    var body: some View {
        HSplitView {
            
            // sidebar
            List(selection: $selectedRecipe) {
                ForEach(listedRecipes) { recipe in
                    Text(recipe.name)
                        .tag(recipe) // <--- here
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            // main view
            Form {
                Section("Recipe") {
                    TextField("Name", text: $selectedRecipe.name)
                }
                Button {
                    // <--- here, to save
                    if let index = listedRecipes.firstIndex(where: {$0.id == selectedRecipe.id}) {
                        listedRecipes[index].name = selectedRecipe.name
                    }
                } label: {
                    Text("Save")
                }
            }
            .formStyle(.grouped)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .onAppear {
            // If there's no selection but we have recipes, select the first one
            if !listedRecipes.isEmpty, let recipe = listedRecipes.first {
                selectedRecipe = recipe
            }
        }
    }
}

Sign up to request clarification or add additional context in comments.

3 Comments

This is doesn't solve the cursor issue for me. Also, there is another issue in your code: after start editing (when the cursor jumps to the end of the field), the selection in the list is going to lost. Anyway, thanks for the answer.
The issues you mention do not happen for me with my code. Tested on macOS Sequoia 15.5, using Xcode 16.3. I guess it does not behave the same on older macOS.
Yes, it looks like SwiftUI has a bug on older macOS/Xcode. Thanks for testing it out.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.