Reload TextField keyboard in SwiftUI - swiftui

I am trying to change the keyboard type of a single TextField dynamically. That is, I have something like this:
struct ContentView: View {
#Binding var keyboardType: UIKeyboardType
#State var searchText = ""
var body: some View {
TextField("placeholder", text: $searchText).keyboardType(keyboardType)
}
}
Now, dynamically, I would like the keyboard to change from one keyboard style to the other, depending on what keyboardType is. However, what I've found is that keyboardType changes, but the soft keyboard doesn't. Rather, the keyboard stays the same, and only after I dismiss the keyboard and bring it back up, does it show something different.
I see that there is a way to wrap UITextField so that the keyboard updates dynamically. But I was wondering/hoping there'd be a way to do this in SwiftUI. Perhaps there's a way to access UITextView's reloadInputViews() method? Any help would be appreciated.

You can use #FocusState – disable focus, change the keyboard, then focus again:
struct ContentView: View {
#State var mykeyboard = UIKeyboardType.numberPad
#State var searchText = ""
#FocusState var focus: Bool
var body: some View {
VStack {
Divider()
HStack {
TextField("placeholder", text: $searchText)
.keyboardType(mykeyboard)
.focused($focus)
Button("Submit") {
focus = false
}
}
Divider()
HStack {
Button("Numbers") {
focus = false
mykeyboard = .numberPad
focus = true
}
Button("EMail") {
focus = false
mykeyboard = .emailAddress
focus = true
}.padding()
Button("Alphabet") {
focus = false
mykeyboard = .alphabet
focus = true
}
}
}
.padding()
.buttonStyle(.bordered)
}
}

Related

NavigationStack Searchable .focused

I need to configure when the user clicks in the Search box to fulfill the condition (display another View). Once he clicks Cancel to display the original view (which can already be tested via .onChange(of: searchText) { value in if (!value.isEmpty) {...)
NavigationStack {
...
if showView == true {}
...
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: LocalizedStringKey("Look for something"))
.focused($focusState)
.onChange(of: focusState, perform: {
showView = true
})
When the user starts searching, I need to show a different View and hide the original one because I have search settings on the new one. As soon as he clicks on the Search button, the search starts.
#FocusState isn't the way to handle this, as the search bar does update or respond to changes in this state.
What you need to use is the isSearching Environment variable in the view on which the .searchable modifier is applied, for example:
struct ContentView: View {
#State private var searchText = ""
var body: some View {
NavigationView {
SearchingView(searchText: $searchText)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: LocalizedStringKey("Look for something"))
}
}
}
struct SearchingView: View {
#Environment(\.isSearching) private var isSearching
#Binding var searchText: String
var body: some View {
if isSearching {
// Show your filtered data view
} else {
// Show non-searching view
}
}
}

TextField in sheet that accepts focus immediately without any noticeable delay

I want to open a Sheet in SwiftUI that 1) contains a TextField which 2) accepts focus immediately. Unfortunately, as demonstrated in the attached clip, there’s a short but noticeable delay before the keyboard pops up. This makes the transition into the focused state not as smooth as I would like (UI shifting to make room for the keyboard).
Minimum reproducible example:
struct TestRootView: View {
#State private var sheet = false
var body: some View {
Button("Open sheet") {
sheet.toggle()
}
.sheet(isPresented: $sheet) {
SheetView()
}
}
}
struct SheetView: View {
#Environment(\.dismiss) var dismiss
#FocusState var isFocused
#State var text: String = ""
var body: some View {
TextField("title", text: $text)
.focused($isFocused)
.onAppear {
isFocused = true
}
Button("Close") {
dismiss()
}.font(.title)
.padding()
.background(.black)
}
}

SwiftUI List with #FocusState and focus change handling

I want to use a List, #FocusState to track focus, and .onChanged(of: focus) to ensure the currently focused field is visible with ScrollViewReader. The problem is: when everything is setup together the List rebuilds constantly during scrolling making the scrolling not as smooth as it needs to be.
I found out that the List rebuilds on scrolling when I attach .onChanged(of: focus). The issue is gone if I replace List with ScrollView, but I like appearance of List, I need sections support, and I need editing capabilities (e.g. delete, move items), so I need to stick to List view.
I used Self._printChanges() in order to see what makes the body to rebuild itself when scrolling and the output was like:
ContentView: _focus changed.
ContentView: _focus changed.
ContentView: _focus changed.
ContentView: _focus changed.
...
And nothing was printed from the closure attached to .onChanged(of: focus). Below is the simplified example, the smoothness of scrolling is not a problem in this example, however, once the List content is more or less complex the smooth scrolling goes away and this is really due to .onChanged(of: focus) :(
Question: Are there any chances to listen for focus changes and not provoke the List to rebuild itself on scrolling?
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
#FocusState var focus: Field?
#State var text: String = ""
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100) {
TextField("Enter the text for \($0)", text: $text)
.id(Field.fieldId($0))
.focused($focus, equals: .fieldId($0))
}
}
.onChange(of: focus) { _ in
print("Not printed unless focused manually")
}
}
}
if you add printChanges to the beginning of the body, you can monitor the views and see that they are being rendered by SwiftUI (all of them on each focus lost and focus gained)
...
var body: some View {
let _ = Self._printChanges() // <<< ADD THIS TO SEE RE-RENDER
...
so after allot of testing, it seams that the problem is with .onChange, once you add it SwiftUI will redraw all the Textfields,
the only BYPASS i found is to keep using the deprecated API as it works perfectly, and renders only the two textfields (the one that lost focus, and the one that gained the focus),
so the code should look this:
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
// #FocusState var focus: Field? /// NO NEED
#State var text: String = ""
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100) {
TextField("Enter the text for \($0)", text: $text)
.id(Field.fieldId($0))
// .focused($focus, equals: .fieldId($0)) /// NO NEED
}
}
// .onChange(of: focus) { _ in /// NO NEED
// print("Not printed unless focused manually") /// NO NEED
// } /// NO NEED
.focusable(true, onFocusChange: { focusNewValue in
print("Only textfileds that lost/gained focus will print this")
})
}
}
I recommend to consider separation of list row content into standalone view and use something like focus "selection" approach. Having FocusState internal of each row prevents parent view from unneeded updates (something like pre-"set up" I assume).
Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
#State private var inFocus: Field?
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100, id: \.self) {
ExtractedView(i: $0, inFocus: $inFocus)
}
}
.onChange(of: inFocus) { _ in
print("Not printed unless focused manually")
}
}
struct ExtractedView: View {
let i: Int
#Binding var inFocus: Field?
#State private var text: String = ""
#FocusState private var focus: Bool // << internal !!
var body: some View {
TextField("Enter the text for \(i)", text: $text)
.focused($focus)
.id(Field.fieldId(i))
.onChange(of: focus) { _ in
inFocus = .fieldId(i) // << report selection outside
}
}
}
}

SwiftUI .fullScreenCover appears as a .sheet

I expected to be able to dismiss a sheet and present a fullScreenCover straight after, however this doesn't seem to work without a delay between the two state modifications.
struct ContentView: View {
#State var sheet = false
#State var cover = false
var body: some View {
Button("Click me for sheet") {
sheet = true
}
.fullScreenCover(isPresented: $cover) {
Text("This is a full screen cover")
}
.sheet(isPresented: $sheet) {
Text("This is a sheet")
Button("This doesn't work") {
sheet = false
cover = true
}
Button("This works") {
sheet = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
cover = true
}
}
}
}
}
Clicking the "This doesn't work" button produces the below image demonstrating a fullScreen cover displaying as a sheet and not covering the entire screen:
Introducing a small delay between the two state modifications which is done by clicking the "This works" button fixes the issue.
What am I not understanding about SwiftUI here that would explain this behaviour?
Tested on:
Xcode 12.5.1
iPhone 12 Pro Max Simulator running iOS 14.5
Adding the modifiers to seperate view like this also doesn't work:
Text("Another view").sheet(isPresented: $sheet) { ...
you could try this:
struct ContentView: View {
#State var sheet = false
#State var cover = false
var body: some View {
Button("Click me for sheet") {
sheet = true
}
.fullScreenCover(isPresented: $cover) {
Text("This is a full screen cover")
}
.sheet(isPresented: $sheet, onDismiss: {cover = true}) {
Text("This is a sheet")
}
}
}

Why does this SwiftUI code show "Empty" in the destination screen?

struct ContentView: View {
#State var showModal = false
#State var text = "Empty"
var body: some View {
Button("show text") {
text = "Filled"
showModal = true
}
.sheet(isPresented: $showModal) {
VStack {
Text(text)
Button("print text") {
print(text)
}
}
}
}
}
I thought that when the "show text" button was tapped, the value of text would be set to "Filled" and showModal would be set to true, so that the screen specified in sheet would be displayed and the word "Filled" would be shown on that screen.
I thought it would show "Filled", but it actually showed "Empty".
Furthermore, when I printed the text using the print text button, the console displayed "Filled".
Why does it work like this?
What am I missing to display the value I set when I tap the button on the destination screen?
using Xcode12.4, Xcode12.5
Add the code for the new pattern.
struct ContentView: View {
#State var number = 0
#State var showModal = false
var body: some View {
VStack {
Button("set number 1") {
number = 1
showModal = true
print("set number = \(number)")
}
Button("set number 2") {
number = 2
showModal = true
print("set number = \(number)")
}
Button("add number") {
number += 1
showModal = true
print("add number = \(number)")
}
}
.sheet(isPresented: $showModal) {
VStack {
let _ = print("number = \(number)")
Text("\(number)")
}
}
}
}
In the above code, when I first tap "set number 1" or "set number 2", the destination screen shows "0". No matter how many times you tap the same button, "0" will be displayed.
However, if you tap "set number 2" after tapping "set number 1", it will work correctly and display "2". If you continue to tap "set number 1", "1" will be displayed and the app will work correctly.
When you tap "add number" for the first time after the app is launched, "0" will still be displayed, but if you tap "add number" again, "2" will be displayed and the app will count up correctly.
This shows that the rendering of the destination screen can be started even when the #State variable is updated, but only when the #State variable is referenced first in the destination screen, it does not seem to be referenced properly.
Can anyone explain why it behaves this way?
Or does this look like a bug in SwiftUI?
Since iOS 14, there are a couple of main ways of presenting a Sheet.
Firstly, for your example, you need to create a separate View and pass your property to a Binding, which will then be correctly updated when the Sheet is presented.
// ContentView
Button { ... }
.sheet(isPresented: $showModal) {
SheetView(text: $text)
}
struct SheetView: View {
#Binding var text: String
var body: some View {
VStack {
Text(text)
Button("print text") {
print(text)
}
}
}
}
The other way of doing it is by using an Optional identifiable object, and when that object has a value the sheet will be presented. Doing that, you do not need to separately manage the state of whether the sheet is showing.
struct Item: Identifiable {
var id = UUID()
var text: String
}
struct ContentView: View {
#State var item: Item? = nil
var body: some View {
Button("show text") {
item = Item(text: "Filled")
}
.sheet(item: $item, content: { item in
VStack {
Text(item.text)
Button("print text") {
print(item.text)
}
}
})
}
}
By adding the following code, I was able to display the word "Filled" in the destination screen.
Declare the updateDetector as a #State variable.
Update the updateDetector onAppear of the View in the sheet.
Reference the updateDetector somewhere in the View in the sheet.
struct ContentView: View {
#State var showModal = false
#State var updateDetector = false
#State var text = "Empty"
var body: some View {
Button("show text") {
text = "Filled"
print("set text to \(text)")
showModal = true
}
.sheet(isPresented: $showModal) {
let _ = print("render text = \(text)")
VStack {
Text(text)
// use EmptyView to access updateDetector
if updateDetector {
EmptyView()
}
}
.onAppear {
// update updateDetector
print("toggle updateDetector")
updateDetector.toggle()
}
}
}
}
Then, after rendering the initial value at the time of preloading, the process of updating the updateDetector onAppear will work, the View will be rendered again, and the updated value of the #State variable will be displayed.
When you tap the "show text" button in the above code, the following will be displayed in the console and "Filled" will be shown on the screen.
set text to Filled
render text = Empty
toggle updateDetector
render text = Filled
I'm still not convinced why I can't get the updated value of the #State variable in the first preloaded sheet. So I will report this to Apple via the Feedback Assistant.