Setting the cursor position within a SwiftUI TextEditor - swift

Is there any way to programmatically move the cursor to a specific text line or select it within a SwifUI TextEditor?
For example, if there is a TextEditor with 10 lines written in it.
When the user presses a button, the cursor will navigate to, or the text will be selected on the 3rd line.

This is currently not possible using the default SwiftUI TextEditor. You can achieve the desired behavior by wrapping NSTextField(/UITextField) in a NSViewRepresentable(/UIViewRepresentable).
I recently implemented this for CodeEditor. You can check out the implementation there (especially the changes from my PR).
However, since you mentioned code in one of the comments, you might also just want to use CodeEditor as a whole.
With my implementation, you can provide the editor with a binding to a Range<String.Index>.
struct ContentView: View {
static private let initialSource = "let a = 42\n"
#State private var source = Self.initialSource
#State private var selection = Self.initialSource.endIndex..<Self.initialSource.endIndex
var body: some View {
CodeEditor(source: $source,
selection: $selection,
language: .swift,
theme: .ocean,
autoscroll: true)
Button("Select All") {
selection = source.startIndex..<source.endIndex
}
}
}
You can move the cursor by updating selection. If the range is "empty" you just move the cursor to the start index. Otherwise the characters from start (included) to end index (excluded) get selected.
The solutions provided here should allow you to find the right String.Index for the line you want to place your cursor in.
If you want to select the whole line, scan the string from this String.Index in both directions until you find a newline character.

Related

Typing in TextField moves cursor to end (macOS)

In macOS Ventura 13.0.1 and Xcode 14.1, I'm experiencing an issue where when I type in the beginning of a TextField, the cursor for some reason moves to the back.
I was able to reproduce it very simply:
struct ContentView: View {
#State private var text = "DEFG"
var body: some View {
NavigationSplitView {
Text("Sidebar")
} detail: {
VStack {
TextField("Text", text: $text)
}
}
}
}
To reproduce:
Open the app, and move the cursor to the start of the TextField. (so the insertion point is before the "D".
Type a letter, like "A", and the insertion point should jump to the end for some reason.
I have determined the cause is the VStack. For some reason, removing it fixes this issue entirely. However, in my real app, I do need the VStack as I have other elements too.
I wonder what's going on here. Is there some major oversight I have made? Or could this be an issue with SwiftUI?
If anyone is having trouble reproducing, I can upload a video.

SwiftUI on iOS 16 - Multiple selection in a List does not work

This is on iOS 16. I'm on Xcode 14.0.
I have the following view:
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
let id = UUID()
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var multiSelection = Set<UUID>()
var body: some View {
NavigationView {
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.toolbar { EditButton() }
}
Text("\(multiSelection.count) selections")
}
}
This code is taken from https://developer.apple.com/documentation/SwiftUI/List.
I am expecting to see that whenever I click on the "Edit" button, I should be able to select a few items, press "Done", then the bottom would still show the number of items I have selected. However, this is not the case:
I tried to use a debugger, and I found out that whenever I click on "Done" after selecting the items, the multiSelection resets itself to be empty. This used to work on Xcode 13. I can't really find anything on Apple's documentation regarding changes to the EditButton or changes to the List struct.
Update
I filed a bug report and Apple got back to me, they said this is expected behaviour. I guess I misinterpreted the use case for this list selection here.
Before iOS 16 the selection only worked when in editing mode. Now it works also when not editing so I believe the problem is now the selection is being cleared when done is being tapped (so it can be used when not editing).
I think we need to send feedback to request 2 selection bindings, one for editing and one for when not-editing.
i think the issue here is that your identifiers for your list are not stable. Basically every time an ocean object is created, it gets a new UUID. You dont want that UUID to change.
Any time your state property changes, the view may or may not get rebuilt, causing your oceans to get regenerated.
Try storing your oceans in like this:
#State var oceans: [Ocean] = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
Alternatively, you can use the ocean's name as its identifier instead of a UUID that is generated each time its created.

How does this SwiftUI binding + state example manage to work without re-invoking body?

A coworker came up with the following SwiftUI example which looks like it works just as expected (you can enter some text and it gets mirrored below), but how it works is surprising to me!
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
TextField("Change the string", text: $text)
WrappedText(text: $text)
}
}
}
struct WrappedText: View {
#Binding var text: String
var body: some View {
Text(text)
}
}
My newbie mental model of SwiftUI led me to think that typing in the TextField would change the $text binding, which would in turn mutate the text #State var. This would then invalidate the ContentView, triggering a fresh invocation of body. But interestingly, that's not what happens! Setting a breakpoint in ContentView's body only gets hit once, while WrappedText's body gets run every time the binding changes. And yet, as far as I can tell, the text state really is changing.
So, what's going on here? Why doesn't SwiftUI re-invoke ContentView's body on every change to text?
On State change SwiftUI rendering engine at first checks for equality of views inside body and, if some of them not equal, calls body to rebuild, but only those non-equal views. In your case no one view depends (as value) on text value (Binding is like a reference - it is the same), so nothing to rebuild at this level. But inside WrappedText it is detected that Text with new text is not equal to one with old text, so body of WrappedText is called to re-render this part.
This is declared rendering optimisation of SwiftUI - by checking & validating exact changed view by equality.
By default this mechanism works by View struct properties, but we can be involved in it by confirming our view to Eqatable protocol and marking it .equatable() modifier to give some more complicated logic for detecting if View should be (or not be) re-rendered.

SwiftUI TextField keyboard flashes and breaks typing in other languages

https://youtu.be/ngExUJ7gyb8
Please observe the different behavior between system keyboard and the one using SwiftUI.
The first 5 seconds using spotlight search, you'll notice the typing is complete and the word picker does not flash for every character input.
In the last 5 second, you'll notice using SwiftUI's keyboard, the typing will be break every couple characters, and the word picker flashes for every new character input.
The same behavior can be observed using the simplest code
struct ContentView: View {
#State private var inputText = ""
var body: some View {
VStack(spacing: 10.0) {
TextField("Tap here", text: $inputText)
}
}
}
Is that a bug of SwiftUI? Or did I miss something?

SwiftUI macOS Scroll a List With Arrow Keys While a TextField is Active

I'd like to use a SwiftUI TextField and a SwiftUI List to render a "search box" above a list of items. Something roughly like the search box available in Safari's Help menu item...which provides a search box where you can always enter text while simultaneously browsing through the list of results using the up and down arrow keys.
I've played with onMoveCommand, focusable, and adjustments to the "parent" NSWindow, but haven't found a clear and obvious way for the TextField to constantly accept input while still being able to navigate the underlying List using the up and down arrow keys. The following code allows for either text to be entered in the TextField, or list entries to be navigated through, but not both at the same time...
struct ContentView: View {
#State var text: String = ""
#State var selection: Int? = 1
var body: some View {
VStack {
TextField("Enter text", text: $text)
List(selection: $selection) {
ForEach((1...100), id: \.self) {
Text("\($0)")
}
}
}
}
}
You can wrap a NSTextField in a NSViewRepresentable and use the NSTextFieldDelegate to intercept the move up and down keys. You can take a look into my suggestions demo.
Source of a text field with suggestions