Clearing SwiftUI TextField will not restore placeholder - swift

I have a SwiftUI screen with three textfields. When you run the code and tap the Clear button, you'll see three completely empty textfields. Expected is that you'd see the placeholder text, but that only appears in each textfield when it receives focus (i.e. user taps inside the field).
class UserInput: ObservableObject {
#Published var text1 = "some text"
#Published var text2 = "some more text"
#Published var text3 = "and this is the final input"
func clear() {
self.text1 = ""
self.text2 = ""
self.text3 = ""
}
}
struct ContentView: View {
#ObservedObject var userInput = UserInput()
var body: some View {
Form {
TextField("Type something in text1", text: self.$userInput.text1)
TextField("Type something in text2", text: self.$userInput.text2)
TextField("Type something in text3", text: self.$userInput.text3)
Button("Clear all fields", action: self.userInput.clear)
}
}
}
Is there something I'm missing, or is there a workaround for this behavior?

I found a workaround. Basically I send a special character that the user could never type, then catch that and clear the field "locally", in the form itself. It works, and restores the placeholder as one would expect.
As workarounds go, this one is pretty ugly.
class UserInput: ObservableObject {
static let clearCode = String.Element(Unicode.Scalar(7))
#Published var text1 = "some text"
#Published var text2 = "some more text"
#Published var text3 = "and this is the final input"
func clear() {
self.text1 = String(Self.clearCode)
self.text2 = String(Self.clearCode)
self.text3 = String(Self.clearCode)
}
}
struct ContentView: View {
#ObservedObject var userInput = UserInput()
var body: some View {
Form {
TextField("Type something in text1", text: self.$userInput.text1)
.onReceive(self.userInput.text1.publisher) { newValue in
if newValue == UserInput.clearCode {
self.userInput.text1 = ""
}
}
TextField("Type something in text2", text: self.$userInput.text2)
.onReceive(self.userInput.text2.publisher) { newValue in
if newValue == UserInput.clearCode {
self.userInput.text2 = ""
}
}
TextField("Type something in text3", text: self.$userInput.text3)
.onReceive(self.userInput.text3.publisher) { newValue in
if newValue == UserInput.clearCode {
self.userInput.text3 = ""
}
}
Button("Clear all fields", action: self.userInput.clear)
}
}
}
I tried the following solution but that didn't provide a workaround and still leaves the placeholder cleared.
class UserInput: ObservableObject {
let clearPublisher = PassthroughSubject<Bool, Never>()
// ...
func clear() {
self.clearPublisher.send(true)
}
}
struct ContentView: View {
// ...
TextField("Type something in text1", text: self.$userInput.text1)
.onReceive(self.userInput.clearPublisher) { _ in
self.userInput.text1 = ""
}
// ...

Related

Unable to store selected Picker value in SwiftUI

I have a simple SwiftUI view with a Picker containing a list of objects from a data array. The Picker lists the objects just fine, but the selected value is not being saved to the binding variable $selectedCar. It returns empty string. This is the view in question:
struct GarageSpace: View {
var currentUserID: String
#Environment(\.presentationMode) var presentationMode
#Binding var selectedPlaceID: String
#Binding var selectedPlaceName: String
#Binding var selectedPlaceDate: Date
#Binding var selectedSpaceID: String
#State var selectedCar: String
#Binding var cars: CarArrayObject
var body: some View {
VStack{
Group{
Picker("Car", selection: $selectedCar) {
if let cars = cars{
ForEach(cars.dataArray, id: \.self) {car in
let year = car.year! as String
let make = car.make as String
let model = car.model! as String
let string = year + " " + make + " " + model
Text(string) //displays correctly in Picker
}
}
}
Spacer()
if let cars = cars {
Button {
print("yes")
print(selectedCar) //returns empty string
} label: {
Text("Confirm")
}
}
}
}
}
}
The above view is displayed via a NavigationLink on the previous screen:
NavigationLink(destination: GarageSpace(currentUserID: currentUserID, selectedPlaceID: $selectedPlaceID, selectedPlaceName: $selectedPlaceName, selectedPlaceDate: $selectedPlaceDate, selectedSpaceID: $selectedSpaceID, selectedCar: "", cars: $cars)) {
}
This NavigationLink might be the culprit because I'm sending an empty string for selectedCar. However, it forces me to initialize a value with the NavigationLink.
Any ideas? Thanks!
EDIT:
Added a tag of type String, still same outcome:
Text(string).tag(car.carID)
EDIT: FOUND THE ISSUE! However, I'm still stumped. The selection variable is empty because I wasn't pressing on the Picker since I only had one item in the array. How can I get the Picker to "select" an item if it's the only one in the array by default?
With tag, all works well in my simple tests. Here is my test code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
GarageSpace()
}
}
struct GarageSpace: View {
#State var selectedCar: String = ""
#State var cars: CarArrayObject? = CarArrayObject(car: CarModel(make: "Ford"))
var body: some View {
VStack {
Group {
Picker("Car", selection: $selectedCar) {
if let cars = cars {
ForEach(cars.dataArray, id: \.self) { car in
Text(car.make).tag(car.carID)
}
}
}
Spacer()
if let cars = cars {
Button {
print("----> selectedCar carID: \(selectedCar)")
} label: {
Text("Show selected carID")
}
}
}
}
// optional, to select the first car
.onAppear {
if let cars = cars {
selectedCar = (cars.dataArray.first != nil) ? cars.dataArray.first!.carID : ""
}
}
}
}
struct CarModel: Hashable {
var make = ""
var carID = UUID().uuidString
}
class CarArrayObject: ObservableObject{
// for testing
#Published var dataArray = [CarModel(make: "Toyota"), CarModel(make: "Suzuki"), CarModel(make: "VW")]
/// USED FOR SINGLE CAR SELECTION
init(car: CarModel) {
self.dataArray.append(car)
}
/// USED FOR GETTING CARS FOR USER PROFILE
init(userID: String) {
// print("GET CARS FOR USER ID \(userID)")
// DataService.instance.downloadCarForUser(userID: userID) { (returnedCars) in
//
// let sortedCars = returnedCars.sorted { (car1, car2) -> Bool in
// return car1.dateCreated > car2.dateCreated
// }
// self.dataArray.append(contentsOf: sortedCars)
// }
}
}

SwiftUI - how to update a item in a struct using a textfield and .onchange

I have the following code
struct ContentView: View {
#ObservedObject var list = ModelList.shared
var body: some View {
NavigationView {
List(list.sorted()) {object in
NavigationLink(destination: ModelView(object: object)) {
Text(object.title)
}
}
}
}
}
struct ModelView: View {
#State var object: ModelObject
var body: some View {
VStack {
Text(object.title)
TextField("Label", text: self.$object.text) // xxxxx Error on this line
.onChange(of: self.$object.text) { newValue in
print("Text changed to \(self.$object.text)!")
}
Button("Use") {
self.object.updateDate = Date()
print("title: \(object.title) - text: \(object.text) - date: \(object.updateDate)")
ModelList.shared.objectWillChange.send()
}
}
}
}
class ModelObject: ObservableObject {
#Published var updateDate: Date = Date()
let title: String
var text: String
init(title: String) {
self.title = title
self.text = ""
print(self)
}
}
I do get the error - Instance method 'onChange(of:perform:)' requires that 'Binding' conform to 'Equatable' on line XXXXX
However if I remove the textfield on change line then it compiles and have the code working. But I want to have some action be done when the Textfield get changed and the data to be saved in the struct in the array?
What am I missing here?
Thank you.
.onChange(of:perform:) doesn't take a Binding. Just pass the value. The same is true in the print statement:
TextField("Label", text: self.$object.text)
.onChange(of: self.object.text) { newValue in // removed $
print("Text changed to \(self.object.text)!") // removed $
}
Here is a minimal testable example that demonstrates the problem:
struct ContentView: View {
#State private var string = "hello"
var body: some View {
TextField("Label", text: self.$string)
.onChange(of: self.$string) { newValue in // remove $ here
print("Text changed to \(self.$string)") // remove $ here
}
}
}

Ignore left whitespaces on imput in TextField SwiftUI Combine

I'm passing to TextField published variable
TextField("First name", text: $state.firstName)
I want to control imputes: Ignore spaces, if it's entered from the left
Where and how can I do it?
It is possible to do with proxy binding, like below
TextField("First name", text: Binding(
get: { self.state.firstName },
set: {
var newValue = $0
// fix newValue here as needed
self.state.firstName = newValue
}))
In your ViewModel add a checker that will automatically check every keystroke and fix the white space at first index.
import Foundation
import Combine
class ViewModel: ObservableObject {
#Published var value: String = ""
var previousAmount = 0.0
var validStringChecker: AnyCancellable? = nil
init() {
validStringChecker = $value.sink { val in
if val.first == " " {
var newValue = val
newValue.remove(at: newValue.firstIndex(of: " ")!)
DispatchQueue.main.async {
self.value = newValue
}
}
}
}
}
Use your TextField in your ContentView like:
import SwiftUI
import Foundation
import Combine
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
TextField("First Name", text: $viewModel.value)
.textFieldStyle(RoundedBorderTextFieldStyle()).padding()
}
}
}

SwiftUI validate input in textfields

I am trying to validate user input in a TextField by removing certain characters using a regular expression. Unfortunately, I am running into problems with the didSet method of the text var calling itself recursively.
import SwiftUI
import Combine
class TextValidator: ObservableObject {
#Published var text = "" {
didSet {
print("didSet")
text = text.replacingOccurrences(
of: "\\W", with: "", options: .regularExpression
) // `\W` is an escape sequence that matches non-word characters.
}
}
}
struct ContentView: View {
#ObservedObject var textValidator = TextValidator()
var body: some View {
TextField("Type Here", text: $textValidator.text)
.padding(.horizontal, 20.0)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
On the swift docs (see the AudioChannel struct), Apple provides an example in which a property is re-assigned within its own didSet method and explicitly notes that this does not cause the didSet method to be called again. I did some testing in a playground and confirmed this behavior. However, things seem to work differently when I use an ObservableObject and a Published variable.
How do I prevent the didSet method from calling itself recursively?
I tried the examples in this post, but none of them worked. Apple may have changed things since then, so this post is NOT a duplicate of that one.
Also, setting the text back to oldValue within the didSet method upon encountering invalid characters would mean that if a user pastes text, then the entire text would be removed, as opposed to only the invalid characters being removed. So that option won't work.
Since SwiftUI 2 you can check the input using the onChange method and do any validations or changes there:
TextField("", value: $text)
.onChange(of: text) { [text] newValue in
// do any validation or alteration here.
// 'text' is the old value, 'newValue' is the new one.
}
Try to validate what you want in the TextField onRecive method like this:
class TextValidator: ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#ObservedObject var textValidator = TextValidator()
var body: some View {
TextField("Type Here", text: $textValidator.text)
.padding(.horizontal, 20.0)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onReceive(Just(textValidator.text)) { newValue in
let value = newValue.replacingOccurrences(
of: "\\W", with: "", options: .regularExpression)
if value != newValue {
self.textValidator.text = value
}
print(newValue)
}
}
}
Here is possible approach using proxy binding, which still also allow separation of view & view model logic
class TextValidator: ObservableObject {
#Published var text = ""
func validate(_ value: String) -> String {
value.replacingOccurrences(
of: "\\W", with: "", options: .regularExpression
)
}
}
struct ContentView: View {
#ObservedObject var textValidator = TextValidator()
var body: some View {
let validatingText = Binding<String>(
get: { self.textValidator.text },
set: { self.textValidator.text = self.textValidator.validate($0) }
)
return TextField("Type Here", text: validatingText)
.padding(.horizontal, 20.0)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
2021 | SwiftUI 2
Custom extension usage:
TextField("New Branch name", text: $model.newNameUnified)
.ignoreSymbols( symbols: [" ", "\n"], string: $model.newNameUnified )
Extension:
#available(OSX 11.0, *)
public extension TextField {
func ignoreSymbols(symbols: [Character], string: Binding<String>) -> some View {
self.modifier( IgnoreSymbols(symbols: symbols, string: string) )
}
}
#available(OSX 11.0, *)
public struct IgnoreSymbols: ViewModifier {
var symbols: [Character]
var string: Binding<String>
public func body (content: Content) -> some View
{
content.onChange(of: string.wrappedValue) { value in
var newValue = value
for symbol in symbols {
newValue = newValue.replace(of: "\(symbol)", to: "")
}
if value != newValue {
string.wrappedValue = newValue
}
}
}
}
Here's what I came up with:
struct ValidatableTextField: View {
let placeholder: String
#State private var text = ""
var validation: (String) -> Bool
#Binding private var sourceText: String
init(_ placeholder: String, text: Binding<String>, validation: #escaping (String) -> Bool) {
self.placeholder = placeholder
self.validation = validation
self._sourceText = text
self.text = text.wrappedValue
}
var body: some View {
TextField(placeholder, text: $text)
.onChange(of: text) { newValue in
if validation(newValue) {
self.sourceText = newValue
} else {
self.text = sourceText
}
}
}
}
Usage:
ValidatableTextField("Placeholder", text: $text, validation: { !$0.contains("%") })
Note: this code doesn't solve specifically your problem but shows how to deal with validations in general.
Change body to this to solve your problem:
TextField(placeholder, text: $text)
.onChange(of: text) { newValue in
let value = newValue.replacingOccurrences(of: "\\W", with: "", options: .regularExpression)
if value != newValue {
self.sourceText = newValue
self.text = sourceText
}
}
Since didSet and willSet are always called when setting values, and objectWillChange triggers an update to the TextField (which triggers didSet again), a loop was created when the underlying value is updated unconditionally in didSet.
Updating the underlying value conditionally breaks the loop.
For example:
import Combine
class TextValidator: ObservableObject {
#Published var text = "" {
didSet {
if oldValue == text || text == acceptableValue(oldValue) {
return
}
text = acceptableValue(text)
}
}
var acceptableValue: (String) -> String = { $0 }
}
import SwiftUI
struct TestingValidation: View {
#StateObject var textValidator: TextValidator = {
let o = TextValidator()
o.acceptableValue = { $0.replacingOccurrences(
of: "\\W", with: "", options: .regularExpression) }
return o
}()
#StateObject var textValidator2: TextValidator = {
let o = TextValidator()
o.acceptableValue = { $0.replacingOccurrences(
of: "\\D", with: "", options: .regularExpression) }
return o
}()
var body: some View {
VStack {
Text("Word characters only")
TextField("Type here", text: $textValidator.text)
Text("Digits only")
TextField("Type here", text: $textValidator2.text)
}
.padding(.horizontal, 20.0)
.textFieldStyle(RoundedBorderTextFieldStyle())
.disableAutocorrection(true)
.autocapitalization(.none)
}
}

Lazy wont work! Cannot use instance member 'field' within property initializer; property initializers run before 'self' is available

I'm trying to make a form that will have something to type in (str), a TextField to type it in to and some text below (var = correct) to let me know if I have it correct so far.
For example, (str) = "hello world", if "hel" is in the text field the text below will read "True".
The issue is that I can't seem to get correct to use (str) and (field). I've seem similar questions on here that are solved with 'lazy', but that wont work, because ("Property 'field' with a wrapper cannot also be lazy").
func stringSlice(string: String, first: Int, last: Int)->String{
var newStr = ""
for number in 0..<string.count{
if first <= number{
if number <= last{
newStr += "\(string[string.index(string.startIndex, offsetBy: number)])"
}
}
}
return newStr
}
func checkSoFar(answer: String, guess: String)->String{
if stringSlice(string: answer, first: 0, last: guess.count) == guess{
return "True"
}
return "False"
}
struct thisView: View{
var str = "Hello world!"
#State private var field = ""
#State private var correct = checkSoFar(answer: str, guess: field)
var body: some View{
Form{
Text(self.str)
TextField("Type the above", text: $field){
}
Text(correct)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
thisView()
}
}
Any ideas would be greatly appreciated. Thank you.
The simplest solution would be to remove the correct property and do
var body: some View{
Form{
Text(self.str)
TextField("Type the above", text: $field){
}
Text(checkSoFar(answer: str, guess: field))
}
}
But maybe this could also be a way forward: to call the checkSoFar function when a button is pressed
struct thisView: View{
var str: String = "Hello World!"
#State private var field: String = ""
#State private var correct: String = ""
init(answer: String) {
str = answer
}
var body: some View{
Form{
Text(self.str)
TextField("Type the above", text: $field){
}
Text(correct)
Button("Try") {
self.correct = checkSoFar(answer: self.str, guess: self.field)
}
}
}
}