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)
})
}
}
Related
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?
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?
Is it possible (also with detours via AppKit) to create text field alerts for macOS in SwiftUI? I found several tutorials on how to do that with UIKit for iOS but you can't use these solutions for macOS.
With SwiftUI 4 it is now possible to simply add a TextField to the .alert() modifier like you would do this with a Button.
struct ContentView: View {
#State private var showingAlert = false
#State private var input = ""
var body: some View {
Button("Show Alert") {
showingAlert = true
}
.alert("Enter your Name", isPresented: $showingAlert) {
TextField("Your Name", text: $input)
Button("OK") { }
}
}
}
I have a SwiftUI button that, when clicked, a sheet displays a confirmation modal. When a button is clicked in that modal to confirm, I make a save to Core Data.
I'm getting one or all of a few nasty results:
The sheet just hangs and becomes unresponsive.
I get a warning: "Modifying state during view update, this will cause undefined behavior".
I get a crash.
Obviously I'm modifying the state becomes I'm deleting a thing, but I'm unclear on how to do this right.
Here is my view where I click delete:
struct MyView: View{
#State var showDeleteModal = false
var body: some View{
Button("Delete"){
showDeleteModal.toggle()
}
.sheet(isPresented: self.$showDeleteModal) {
ModalView(confirm: {
self.showDeleteModal.toggle()
//Save the object in my Core Data stuff
model.saveThing(thing: thing)
})
}
}
}
And here's my modal that has a callback function to confirm the deletion:
struct ModalView: View {
var confirm:() -> Void
var body: some View {
Button("Confirm"){
confirm()
}
}
}
How do I hide the modal and make the save (which removes the view from the screen) without interfering with SwiftUI's state?
I'd suggest using sheet(isPresented:onDismiss:content:) and calling model.saveThing in onDismiss:
struct MyView: View {
#State var showDeleteModal = false
var body: some View {
Button("Delete") {
showDeleteModal.toggle()
}
.sheet(isPresented: self.$showDeleteModal, onDismiss: onDismiss) {
ModalView()
}
}
func onDismiss() {
model.saveThing(thing: thing)
}
}
Then you can dismiss the sheet without knowing the parent's state - just use #Environment(\.presentationMode):
struct ModalView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
Button("Confirm") {
presentationMode.wrappedValue.dismiss()
}
}
}
Note: as sheet can be closed without interacting with the confirm button, you can detect how it was closed using another #State variable - see:
SwiftUI: How to show an alert after a sheet is closed?
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
}