How to be notified when a TextEditor loses focus? - ios

In my SwiftUI app, I would like to be notified when the TextEditor loses focus/has finished editing. Ideally, something like TextField's onCommit callback would be perfect.
Using the new onChange as below does work for receiving every new character that the user types, but I really want to know when they are finished.
#State var notes = ""
...
TextEditor(text: $notes)
.onChange(of: notes, perform: { newString in
print(newString)
})

I figured it out, you can use the UIResponder.keyboardWillHideNotification.
TextEditor(text: $notes)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
print("done editing!")
}

On iOS 15.0, there is now #FocusState.
struct MyView: View {
#State var text: String
#FocusState private var isFocused: Bool
var body: some View {
Form {
TextEditor(text: $text)
.focused($isFocused)
.onChange(of: isFocused) { isFocused in
// do stuff based off focus state change
}
}
}
}

Related

SwiftUI #FocusState - how to give it initial value

I am excited to see the TextField enhancement: focused(...): https://developer.apple.com/documentation/swiftui/view/focused(_:)
I want to use it to show a very simple SwitfUI view that contains only one TextField that has the focus with keyboard open immediately. Not able to get it work:
struct EditTextView: View {
#FocusState private var isFocused: Bool
#State private var name = "test"
// ...
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Enter your name", text: $name).focused($isFocused)
.onAppear {
isFocused = true
}
// ...
Anything wrong? I have trouble to give it default value.
I was also not able to get this work on Xcode 13, beta 5. To fix, I delayed the call to isFocused = true. That worked!
The theory I have behind the bug is that at the time of onAppear the TextField is not ready to become first responder, so isFocused = true and iOS calls becomeFirstResponder behind the scenes, but it fails (ex. the view hierarchy is not yet done setting up).
struct MyView: View {
#State var text: String
#FocusState private var isFocused: Bool
var body: some View {
Form {
TextEditor(text: $text)
.focused($isFocused)
.onChange(of: isFocused) { isFocused in
// this will get called after the delay
}
.onAppear {
// key part: delay setting isFocused until after some-internal-iOS setup
DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {
isFocused = true
}
}
}
}
}
I was also not able to get this work on Xcode 13, beta 5. To fix, I delayed the call to isFocused = true. That worked!
It also works without delay.
DispatchQueue.main.async {
isFocused = true
}
//This work in iOS 15.You can try it.
struct ContentView: View {
#FocusState private var isFocused: Bool
#State private var username = "Test"
var body: some View {
VStack {
TextField("Enter your username", text: $username)
.focused($isFocused).onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
isFocused = true
}
}
}
}
}
I've had success adding the onAppear to the outermost view (in your case NavigationView):
struct EditTextView: View {
#FocusState private var isFocused: Bool
#State private var name = "test"
// ...
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Enter your name", text: $name).focused($isFocused)
}
}
}
.onAppear {
isFocused = true
}
}
// ...
I’m not certain but perhaps your onAppear attached to the TextField isn’t running. I would suggest adding a print inside of the onAppear to confirm the code is executing.
I faced the same problem and had the idea to solve it by embedding a UIViewController so could use viewDidAppear. Here is a working example:
import SwiftUI
import UIKit
struct FocusTestView : View {
#State var presented = false
var body: some View {
Button("Click Me") {
presented = true
}
.sheet(isPresented: $presented) {
LoginForm()
}
}
}
struct LoginForm : View {
enum Field: Hashable {
case usernameField
case passwordField
}
#State private var username = ""
#State private var password = ""
#FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .usernameField)
SecureField("Password", text: $password)
.focused($focusedField, equals: .passwordField)
Button("Sign In") {
if username.isEmpty {
focusedField = .usernameField
} else if password.isEmpty {
focusedField = .passwordField
} else {
// handleLogin(username, password)
}
}
}
.uiKitOnAppear {
focusedField = .usernameField
// If your form appears multiple times you might want to check other values before setting the focus.
}
}
}
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.action = action
return vc
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {
}
}
class UIAppearViewController: UIViewController {
var action: () -> Void = {}
override func viewDidLoad() {
view.addSubview(UILabel())
}
override func viewDidAppear(_ animated: Bool) {
// had to delay the action to make it work.
DispatchQueue.main.asyncAfter(deadline:.now()) { [weak self] in
self?.action()
}
}
}
public extension View {
func uiKitOnAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}
UIKitAppear was taken from this dev forum post, modified with dispatch async to call the action. LoginForm is from the docs on FocusState with the uiKitOnAppear modifier added to set the initial focus state.
It could perhaps be improved by using a first responder method of the VC rather than the didAppear, then perhaps the dispatch async could be avoided.

SwiftUI Schedule Local Notification Without Button?

This may have a very simple answer, as I am pretty new to Swift and SwiftUI and am just starting to learn. I'm trying to schedule local notifications that will repeat daily at a specific time, but only do it if a toggle is selected. So if a variable is true, I want that notification to be scheduled. I looked at some tutorials online such as this one, but they all show this using a button. Instead of a button I want to use a toggle. Is there a certain place within the script that this must be done? What do I need to do differently in order to use a toggle instead of a button?
You can observe when the toggle is turned on and turned off -- In iOS 14 you can use the .onChange modifier to do this:
import SwiftUI
struct ContentView: View {
#State var isOn: Bool = false
var body: some View {
Toggle(isOn: $isOn, label: {
Text("Notifications?")
})
/// right here!
.onChange(of: isOn, perform: { toggleIsOn in
if toggleIsOn {
print("schedule notification")
} else {
print("don't schedule notification")
}
})
}
}
For earlier versions, you can try using onReceive with Combine:
import SwiftUI
import Combine
struct ContentView: View {
#State var isOn: Bool = false
var body: some View {
Toggle(isOn: $isOn, label: {
Text("Notifications?")
})
/// a bit more complicated, but it works
.onReceive(Just(isOn)) { toggleIsOn in
if toggleIsOn {
print("schedule notification")
} else {
print("don't schedule notification")
}
}
}
}
You can find even more creative solutions to observe the toggle change here.

Receive notifications for focus changes between apps on Split View when using SwiftUI

What should I observe to receive notifications in a View of focus changes on an app, or scene, displayed in an iPaOS Split View?
I'm trying to update some data, for the View, as described here, when the user gives focus back to the app.
Thanks.
Here is a solution that updates pasteDisabled whenever a UIPasteboard.changedNotification is received or a scenePhase is changed:
struct ContentView: View {
#Environment(\.scenePhase) private var scenePhase
#State private var pasteDisabled = false
var body: some View {
Text("Some Text")
.contextMenu {
Button(action: {}) {
Text("Paste")
Image(systemName: "doc.on.clipboard")
}
.disabled(pasteDisabled)
}
.onReceive(NotificationCenter.default.publisher(for: UIPasteboard.changedNotification)) { _ in
updatePasteDisabled()
}
.onChange(of: scenePhase) { _ in
updatePasteDisabled()
}
}
func updatePasteDisabled() {
pasteDisabled = !UIPasteboard.general.contains(pasteboardTypes: [aPAsteBoardType])
}
}

SwiftUI dismiss keyboard when tapping segmentedControl

I have a TextField in SwiftUI that needs to use a different keyboard depending on the value of a #State variable determined by a SegementedControl() picker.
How can I dismiss the keyboard (like send an endEditing event) when the user taps a different segment? I need to do this because I want to change the keyboard type and if the textField is the responder, the keyboard won't change.
I have this extension:
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
And I can do something like
UIApplication.shared.endEditing()
But I don't know where or how to call this when the user taps a different segment.
I have tried putting a tapGesture on the Picker and the keyboard does dismiss, but the tap does not pass through to the picker so it does not change.
Code snippet here:
#State private var type:String = "name"
.
.
.
Form {
Section(header: Text("Search Type")) {
Picker("", selection: $type) {
Text("By Name").tag("name")
Text("By AppId").tag("id")
}.pickerStyle(SegmentedPickerStyle())
}
Section(header: Text("Enter search value")) {
TextField(self.searchPlaceHolder, text: $searchValue)
.keyboardType(self.type == "name" ? UIKeyboardType.alphabet : UIKeyboardType.numberPad)
}
}
Attach a custom Binding to the Picker that calls endEditing() whenever it is set:
Section(header: Text("Search Type")) {
Picker("", selection: Binding(get: {
self.type
}, set: { (res) in
self.type = res
UIApplication.shared.endEditing()
})) {
Text("By Name").tag("name")
Text("By AppId").tag("id")
}.pickerStyle(SegmentedPickerStyle())
}
Update since iOS 13 / iPadOS 13 was released.
Since there is now support for multiple windows in one app you need to loop through the UIWindows and end editing one-by-one.
UIApplication.shared.windows.forEach { $0.endEditing(false) }
SwiftUI 2.0
Now it can be done in more elegant way, with .onChange (actually it can be attached to any view, but at TextField looks appropriate, by intention)
TextField("Placeholder", text: $searchValue)
.keyboardType(self.type == "name" ? UIKeyboardType.alphabet : UIKeyboardType.numberPad)
.onChange(of: type) { _ in
UIApplication.shared.endEditing() // << here !!
}
SwiftUI 1.0+
There are much similar to above approaches
a) requires import Combine...
TextField("Placeholder", text: $searchValue)
.keyboardType(self.type == "name" ? UIKeyboardType.alphabet : UIKeyboardType.numberPad)
.onChange(of: type) { _ in
UIApplication.shared.endEditing()
}
.onReceive(Just(type)) { _ in
UIApplication.shared.endEditing() // << here !!
}
b) ... and not
TextField("Placeholder", text: $searchValue)
.keyboardType(self.type == "name" ? UIKeyboardType.alphabet : UIKeyboardType.numberPad)
.onReceive([type].publisher) { _ in
UIApplication.shared.endEditing() // << here !!
}
For SwiftUI 3 use FocusState and .focused(…)

textFieldDidBeginEditing and textFieldDidEndEditing in SwiftUI

how can I use the methods textFieldDidBeginEditing and textFieldDidEndEditing with the default TextField struct by apple.
TextField has onEditingChanged and onCommit callbacks.
For example:
#State var text = ""
#State var text2 = "default"
var body: some View {
VStack {
TextField($text, placeholder: nil, onEditingChanged: { (changed) in
self.text2 = "Editing Changed"
}) {
self.text2 = "Editing Commited"
}
Text(text2)
}
}
The code in onEditingChanged is only called when the user selects the textField, and onCommit is only called when return, done, etc. is tapped.
Edit: When the user changes from one TextField to another, the previously selected TextField's onEditingChanged is called once, with changed (the parameter) equaling false, and the just-selected TextField's onEditingChanged is also called, but with the parameter equaling true. The onCommit callback is not called for the previously selected TextField.
Edit 2:
Adding an example for if you want to call a function committed() when a user taps return or changes TextField, and changed() when the user taps the TextField:
#State var text = ""
var body: some View {
VStack {
TextField($text, placeholder: nil, onEditingChanged: { (changed) in
if changed {
self.changed()
} else {
self.committed()
}
}) {
self.committed()
}
}
}
SwiftUI 3 (iOS 15+)
Since TextField.init(_text:onEditingChanged:) is scheduled for deprecation in a future version it may be best to use #FocusState. This method also has the added benefit of knowing when the TextField is no longer the "first responder" which .onChange(of:) and .onSubmit(of:) alone will not do.
#State private var text = ""
#FocusState private var isTextFieldFocused: Bool
var body: some View {
TextField("Text Field", text: $text)
.focused($isTextFieldFocused)
.onChange(of: isTextFieldFocused) { isFocused in
if isFocused {
// began editing...
} else {
// ended editing...
}
}
}
SwiftUI 2
ios15 and above
The syntax has changed for swiftui2
a modifier onChange is triggered when a property value has changed and onSubmit is triggered when form is submitted ie you press enter
TextField("search", text: $searchQuery)
.onChange(of: searchQuery){ newValue in
print("textChanged")
}
.onSubmit {
print("textSubmitted")
}

Resources