SwiftUI onCommit doesnt move cursor to next textfield - MacOS app - macos

I have two TextFields that use onCommit. Pressing enter saves the value. However, this does not automatically move the cursor to the next TextField. I want it to work like the tab button which moves the cursor to next TextField but this doesn't save the value (due to onCommit requiring enter/return to be pressed). The best solution I have found is using a button but that results in poor usability as I would be using a ForLoop over this view.
struct ModuleDetailView: View {
#Binding var subjectDetails: [Subjects]
#State private var subject = ""
#State private var grade = ""
var body: some View {
VStack {
TextField("Subject", text: $subject, onCommit: appendData)
TextField("Grade", text: $grade, onCommit: appendData)
VStack {
Text("Output")
ForEach(subjectDetails) { subject in
HStack {
Text(subject.name)
Text(subject.grade)
}
}
}
}
}
func appendData() {
if subject != "" && grade != "" {
let module = Subjects(name: subject, grade: grade)
subjectDetails.append(module)
}
}
}
The preview code:
struct ModuleDetailView_Previews: PreviewProvider {
static var previews: some View {
PreviewWrapper()
}
struct PreviewWrapper: View {
#State var modules = [Subjects]()
var body: some View {
ModuleDetailView(subjectDetails: $modules)
}
}
}
both textfields are visible at the same time. What happens is that after I press enter from the first textField the cursor just vanishes, which works differently from when we press TAB - simply moves to the next. And I want it to work similar to how it behaves when TAB is pressed. Therefore, in this case using a firstResponder might not be a good option
Subjects struct:
struct Subjects: Identifiable {
let id = UUID()
var name: String
var grade: String
}

Related

Swiftui TextEditor seems not to respond to state changes

I am having trouble getting the SwiftUI TextEditor to work when it is in a Child View.
This is a small example that demonstrates the issue for me:
import SwiftUI
struct ContentView: View {
#State private var someText: String = "Hello World"
var body: some View {
VStack {
HStack {
Button("Text 1", action: {someText = "hello"})
Button("Text 2", action: {someText = "world"})
}
ViewWithEditor(entry: $someText)
}
}
}
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
When I click the buttons I expected the Editor text to change, however it remains with its initial value.
The print statement show that the "localString" variable is being updated.
Is TextEditor broken or am I missing something fundamental ??
If you move the buttons into the same view as the TextEditor, directly changing local state var it works as expected.
This is being run under MacOS in case it makes a difference.
TIA Alan.
Ok. So a proxy Binding does the job for me. See updated editor view below:
struct ViewWithEditor: View {
#Binding var entry: String
var body: some View
{
let localString = Binding<String> (
get: {
entry
},
set: {
entry = $0
}
)
return VStack {
Text(entry)
TextEditor(text: localString)
}
}
A bit more ugly (proxy bindings just seem clutter), but in some ways simpler..
It allows for the result of the edit to be reviewed / rejected before being pushed into the bound var.
This is occurring because the binding entry var is not actually being used after initialization of ViewWithEditor. In order to make this work without using the proxy binding add onChange to the ViewWithEditor as below:
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
The problem here is that now entry is not updating if localString changes. One could just use the same approach as before:
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
.onChange(of: localString) {
entry = $0
}
}
but why not just use $entry as the binding string for TextEditor?

How to move focus to view which is not TextField

I have a MacOS app which has a lot of TextFields in many views; and one editor view which has to receive pressed keyboard shortcut, when cursor is above. But as I try, I cannot focus on a view which is not text enabled. I made a small app to show a problem:
#main
struct TestFocusApp: App {
var body: some Scene {
DocumentGroup(newDocument: TestFocusDocument()) { file in
ContentView(document: file.$document)
}
.commands {
CommandGroup(replacing: CommandGroupPlacement.textEditing) {
Button("Delete") {
deleteSelectedObject.send()
}
.keyboardShortcut(.delete, modifiers: [])
}
}
}
}
let deleteSelectedObject = PassthroughSubject<Void, Never>()
struct MysticView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.gray.opacity(0.3))
}.focusable()
.onReceive(deleteSelectedObject) { _ in
print ("received")
}
}
}
enum FocusableField {
case wordTwo
case view
case editor
case wordOne
}
struct ContentView: View {
#Binding var document: TestFocusDocument
#State var wordOne: String = ""
#State var wordTwo: String = ""
#FocusState var focus: FocusableField?
var body: some View {
VStack {
TextField("one", text: $wordOne)
.focused($focus, equals: .wordOne)
TextEditor(text: $document.text)
.focused($focus, equals: .editor)
///I want to receive DELETE in any way, in a MystickView or unfocus All another text views in App to not delete their contents
MysticView()
.focusable(true)
.focused($focus, equals: .view)
.onHover { inside in
focus = inside ? .view : nil
/// focus became ALWAYS nil, even set to `.view` here
}
.onTapGesture {
focus = .view
}
TextField("two", text: $wordTwo)
.focused($focus, equals: .wordTwo)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(document: .constant(TestFocusDocument()))
}
}
Only first TextField became focused when I click or hover over MysticView
I can assign nil to focus, but it will not unfocus fields from outside this one view.
Is it a bug, or I missed something? How to make View focusable? To Unfocus all textFields?

SwiftUI run function on losing focus TextField

I want to run some function whenever a textfield loses focus.
Textfield already has onEditingChange and onCommit but I want to run a validation function once the user leaves the textfield.
Something similar to what onblur does for web without cluttering any other view on screen.
In iOS 15, #FocusState and .focused can give you this functionality. On iOS 13 and 14, you can use onEditingChanged (shown in the second TextField)
struct ContentView: View {
#State private var text = ""
#State private var text2 = ""
#FocusState private var isFocused: Bool
var body: some View {
TextField("Text goes here", text: $text)
.focused($isFocused)
.onChange(of: isFocused) { newValue in
if !newValue {
print("Lost focus")
}
}
TextField("Other text", text: $text2, onEditingChanged: { newValue in
print("Other text:",newValue)
})
}
}

Display subview from an array in SwiftUI

I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.

Does SwiftUI have a Picker with typing to filter a big list?

I wondering if I need to build this myself or if SwiftUI (or AppKit, I can use NSViewRepresentable) has something like it already. This is for a macOS app.
The user needs to choose from a very large list. In the example below I used animal names. There are dozens or maybe 100's of items. They can type some characters to narrow the list. Then they can click any item to choose it, or hit return to select the highlighted item, which could be the first item in the list, or maybe a recently used item.
Single filtering list written in SwiftUI:
let animals = ["Cat", "Camel", "Dog", "Crocodile"]
struct ContentView: View {
#State private var searchString = ""
var body: some View {
VStack {
TextField("Search", text: $searchString)
List(animals.filter({searchString == "" ? true : $0.contains(searchString)}), id: \.self) { animal in
Text(animal)
}
}
}
}
You can increase the model complexity according to your needs, and apply List selection (see documentation of SwiftUI). To support selection:
struct ContentView: View {
#State private var searchString = ""
#State private var listSelection: String? = ""
var body: some View {
VStack {
TextField("Search", text: $searchString)
List(animals.filter({searchString == "" ? true : $0.contains(searchString)}), id: \.self, selection: $listSelection) { animal in
Text(animal)
}
}.padding()
.onChange(of: searchString, perform: { value in
listSelection = animals.filter({searchString == "" ? true : $0.contains(searchString)}).first
})
}
}

Resources