I'm using SwiftUI, but I'm coding my own custom text mask, but I need to delete when a user press the "delete" key. I'm using the onChange method, but it is not detecting when special keys are pressed. Currently I'm using:
TextField(self.placeholder, text: self.$text)
.onChange(of: self.text, perform: { value in
print(value)
})
Is there a way to detect if the delete button is pressed?
Or should I use the UITextField instead of TextField ?
Well you could use a hacky way for this.
First we will hold a count of the current characters the text string has.
Whenever the user presses backspace we check in the onChange handler if the previous character count is higher than the new character count
if that is the case we delete the whole string, or whatever you want to do when the delete button is pressed.
import SwiftUI
struct SquareView: View {
var placeholder = "test"
#State var text = "test"
#State var textLen = 4
var body: some View {
VStack {
TextField(self.placeholder, text: self.$text)
.onChange(of: self.text, perform: { value in
if value.count < textLen {
self.text = "" // << removed the whole text but here you can insert anything you want to do when the delete button is pressed
}
textLen = value.count
})
}
}
}
Keep in mind that this is a hacky way and brings risks. For example if users paste something which is shorter than the current text.
MVVM + Combine way:
class RecoveryPasswordViewModel: ObservableObject {
#Published var email: String = ""
#Published var isValidTextFieldStatus = true
var cancellables = Set<AnyCancellable>()
init() {
setTextFieldToValidWhenUserDelete()
}
func setTextFieldToValidWhenUserDelete() {
$email
.scan("", { [weak self] previousInput, currentInput in
if previousInput.count > currentInput.count {
self?.isValidTextFieldStatus = true
}
return currentInput
})
.sink { _ in }
.store(in: &cancellables)
}
}
Related
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)
}
}
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.
I have a textfield to enter search terms, plus a segmented control to indicate what database table should be searched:
struct ContentView: View {
enum SearchMode: Int {
case searchInNumbers, searchInAlphabet
}
#State private var mode = SearchMode.searchInNumbers
#State private var searchString = ""
private var keyboardType: UIKeyboardType {
if self.mode == .searchInNumbers {
NSLog("Setting keyboard to numbersAndPunctuation")
return .numbersAndPunctuation
} else {
NSLog("Setting keyboard to default")
return .default
}
}
var body: some View {
VStack {
TextField("Search string", text: self.$searchString)
.keyboardType(self.keyboardType)
.textFieldStyle(RoundedBorderTextFieldStyle())
Picker("Search in", selection: self.makePickerBinding()) {
Text("numbers").tag(0)
Text("alphabet").tag(1)
}.pickerStyle(SegmentedPickerStyle())
}.padding(20)
}
private func makePickerBinding() -> Binding<Int> {
Binding<Int>(
get: {
return self.mode.rawValue
},
set: { tag in
self.mode = SearchMode.init(rawValue: tag)!
}
)
}
}
The keyboard style should be the right one for the database table we search. This works, except when the keyboard is already up.
Meaning, when the user first taps the textfield, and then changes the segmented control, the keyboard type does not change.
I don't want to fix it with yet another wrapped UITextField. Can it be fixed in SwiftUI?
I don’t understand why the Text Value doesn’t change. if I remove the TextField, the Text value change :/ is there something about combine or SwiftUI I am missing ?
struct ContentView2: View{
#State private var numTouches: Int = 0
#State private var num: String = ""
var body: some View {
VStack{
Button("Touch me pls"){
self.numTouches += 1
}
Text("\(numTouches)")
TextField("Hello enter a number", text: $num)
}.onReceive(Just(num)) { newValue in
if newValue == "" {
self.numTouches = 0
} else {
self.numTouches = Int.init(newValue)!
}
}
}
}
What happens is that when a button is touched, it increases numTouches, which causes the view's body to re-render. .onReceive subscribes to the Just publisher that immediately publishes the value num, which is empty "" in the beginning, and that sets numTouches back to 0.
It sounds that you have really just a single variable, which is being updated from two places:
via TextField
via Button's action
So, keep it as single #State var numTouches: Int:
struct ContentView2: View{
#State private var numTouches: Int = 0
var body: some View {
VStack{
Button("Touch me pls"){
self.numTouches += 1
}
Text("\(numTouches)")
TextField("Hello enter a number",
text: $numTouches, formatter: NumberFormatter()))
// .keyboardType(.numberPad) // uncomment for number pad keyboard
}
}
}
I want to allow the user to filter data in a long list to more easily find matching titles.
I have placed a TextView inside my navigation bar:
.navigationBarTitle(Text("Library"))
.navigationBarItems(trailing: TextField("search", text: $modelData.searchString)
I have an observable object which responds to changes in the search string:
class DataModel: ObservableObject {
#Published var modelData: [PDFSummary]
#Published var searchString = "" {
didSet {
if searchString == "" {
modelData = Realm.studyHallRealm.objects(PDFSummary.self).sorted(by: { $0.name < $1.name })
} else {
modelData = Realm.studyHallRealm.objects(PDFSummary.self).sorted(by: { $0.name < $1.name }).filter({ $0.name.lowercased().contains(searchString.lowercased()) })
}
}
}
Everything works fine, except I have to tap on the field after entering each letter. For some reason the focus is taken away from the field after each letter is entered (unless I tap on a suggested autocorrect - the whole string is correctly added to the string at once)
The problem is in rebuilt NavigationView completely that result in dropped text field focus.
Here is working approach. Tested with Xcode 11.4 / iOS 13.4
The idea is to avoid rebuild NavigationView based on knowledge that SwiftUI engine updates only modified views, so using decomposition we make modifications local and transfer desired values only between subviews directly not affecting top NavigationView, as a result the last kept stand.
class QueryModel: ObservableObject {
#Published var query: String = ""
}
struct ContentView: View {
// No QueryModel environment object here -
// implicitly passed down. !!! MUST !!!
var body: some View {
NavigationView {
ResultsView()
.navigationBarTitle(Text("Library"))
.navigationBarItems(trailing: SearchItem())
}
}
}
struct ResultsView: View {
#EnvironmentObject var qm: QueryModel // << injected here from top
var body: some View {
VStack {
Text("Search: \(qm.query)") // receive query string
}
}
}
struct SearchItem: View {
#EnvironmentObject var qm: QueryModel // << injected here from top
#State private var query = "" // updates only local view
var body: some View {
let text = Binding(get: { self.query }, set: {
self.query = $0; self.qm.query = $0; // transfer query string
})
return TextField("search", text: text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(QueryModel())
}
}