SwiftUI - conditional onChange - ios

I have 2 TextFields:
__ x2 __
I want to perform simple calculations: string1 x 2 = string2. I am using .onChange modifier, so if you type first number it is multiplied by 2 and result is printed in second TextField. You can go the other way around and type string2 and it will be divided by 2 and result will be printed in the first TextField.
Now because both TextFields have .onChange, it gets triggered few times (3 in this case). After changing string1, string2 gets updated. And as it changed, .onChange of string2 is triggered and later the same with .onChange of string1.
Please run this example code and check what gets printed in console:
import SwiftUI
struct ContentView: View {
#State private var string1: String = ""
#State private var int1: Int = 0
#State private var string2: String = ""
#State private var int2: Int = 0
let multiplier: Int = 2
var body: some View {
VStack {
HStack {
TextField("0", text: $string1)
.keyboardType(.decimalPad)
.onChange(of: string1, perform: { value in
string1 = value
int1 = Int(string1) ?? 0
int2 = int1 * multiplier
string2 = "\(int2)"
print("int1: \(int1)")
})
}
.multilineTextAlignment(.trailing)
.font(.largeTitle)
.background(Color(UIColor.systemGray5))
HStack {
Spacer()
Text("x2")
}
HStack {
TextField("0", text: $string2)
.keyboardType(.decimalPad)
.onChange(of: string2, perform: { value in
string2 = value
int2 = Int(string2) ?? 0
int1 = int2 / multiplier
string1 = ("\(int1)")
print("int2: \(int2)")
})
}
.multilineTextAlignment(.trailing)
.font(.largeTitle)
.background(Color(UIColor.systemGray5))
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Question:
How to make .onChange conditional so it runs only once? To be precise, I want to execute .onChange on first input ONLY when I edit first input. And execute .onChange on second input ONLY when I edit second input.
Probably it will be easy with .onFocus in iOS 15. But how to do it in iOS 14?

I've figured it out. I needed two variables, one per TextField: isFocused1, isFocused2. Each of them changes to true with onEditingChanged. And each onChange has if condition that checks if isFocused for this TextField is true.
Now onChange is triggered only if each TextField is being edited. I have added changing background colors to visualize focus changes.
Working code:
import SwiftUI
struct ContentView: View {
#State private var string1: String = ""
#State private var int1: Int = 0
#State private var string2: String = ""
#State private var int2: Int = 0
let multiplier: Int = 2
#State private var isFocused1 = false
#State private var isFocused2 = false
var body: some View {
VStack {
HStack {
TextField("0", text: $string1, onEditingChanged: { (changed) in
isFocused1 = changed
})
.keyboardType(.decimalPad)
.onChange(of: string1, perform: { value in
if isFocused1 {
int1 = Int(string1) ?? 0
int2 = int1 * multiplier
string2 = "\(int2)"
print("int1: \(int1)")
}
})
.background(isFocused1 ? Color.yellow : Color.gray)
}
.multilineTextAlignment(.trailing)
.font(.largeTitle)
HStack {
Spacer()
Text("x2")
}
HStack {
TextField("0", text: $string2, onEditingChanged: { (changed) in
isFocused2 = changed
})
.keyboardType(.decimalPad)
.onChange(of: string2, perform: { value in
if isFocused2 {
int2 = Int(string2) ?? 0
int1 = int2 / multiplier
string1 = ("\(int1)")
print("int2: \(int2)")
}
})
.background(isFocused2 ? Color.yellow : Color.gray)
}
.multilineTextAlignment(.trailing)
.font(.largeTitle)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
After feedback from #JoakimDanielson I've made a version with enum instead of 2 separate variables:
import SwiftUI
struct ContentView: View {
enum Focus {
case input1
case input2
}
#State private var isFocused: Focus?
#State private var string1: String = ""
#State private var int1: Int = 0
#State private var string2: String = ""
#State private var int2: Int = 0
let multiplier: Int = 2
var body: some View {
VStack {
HStack {
TextField("0", text: $string1, onEditingChanged: { (changed) in
if changed {
isFocused = Focus.input1
}
})
.keyboardType(.decimalPad)
.onChange(of: string1, perform: { value in
if isFocused == .input1 {
int1 = Int(string1) ?? 0
int2 = int1 * multiplier
string2 = "\(int2)"
print("int1: \(int1)")
}
})
.background(isFocused == .input1 ? Color.yellow : Color.gray)
}
.multilineTextAlignment(.trailing)
.font(.largeTitle)
HStack {
Spacer()
Text("x2")
}
HStack {
TextField("0", text: $string2, onEditingChanged: { (changed) in
if changed {
isFocused = Focus.input2
}
})
.keyboardType(.decimalPad)
.onChange(of: string2, perform: { value in
if isFocused == .input2 {
int2 = Int(string2) ?? 0
int1 = int2 / multiplier
string1 = ("\(int1)")
print("int2: \(int2)")
}
})
.background(isFocused == .input2 ? Color.yellow : Color.gray)
}
.multilineTextAlignment(.trailing)
.font(.largeTitle)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

SwiftUI Storing field entry as a double or integer

I'm trying to store a user's text field entry as a number so it's easier to work with in formulas. I thought I was being slick by storing it as a string, but now using the input in mathematical formulas is becoming a real pain in the neck. It should be noted that these field entries are being stored in CoreData currently as a string entity.
Here's an MRE of one of my fields:
import SwiftUI
struct EntryMRE: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var showingResults: Int? = 1
#FocusState private var isTextFieldFocused: Bool
#State var isDone = false
#State var isSaving = false //used to periodically save data
#State var saveInterval: Int = 5 //after how many seconds the data is automatically saved
//DataPoints Chemistry
#State var potassium = ""
var body: some View {
List {
Section(header: Text("🧪 Chemistry")) {
Group {
HStack {
Text("K")
+ Text("+")
.font(.system(size: 15.0))
.baselineOffset(4.0)
Spacer()
TextField("mEq/L", text: $potassium)
.focused($isTextFieldFocused)
.foregroundColor(Color(UIColor.systemBlue))
.modifier(TextFieldClearButton(text: $potassium))
.multilineTextAlignment(.trailing)
.keyboardType(.decimalPad)
if let numberValue = Double(potassium) { // Cast String to Double
if (3.5...5.5) ~= numberValue {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(UIColor.systemGreen))
}
else {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(Color(UIColor.systemRed))
}
}
}
}
}
}
}
}
While not specifically using the $potassium value, here is what I'm currently having to do for formulas:
import SwiftUI
struct ResultView: View {
#Binding var isDone: Bool
var EV: EntryView
let decimalPlaces: Int = 2
var body: some View {
List {
Section(header: Text("Formula")) {
HStack {
Text("Corrected CO2")
Spacer()
Text("\(1.5 * (Double(EV.hCo3) ?? 0) + 8, specifier: "%.\(decimalPlaces)f")")
}
If you want your #State var potassium to be of type Double you can use a different initializer for your TextField.
#State var potassium = 0.0
TextField("mEq/L", value: $potassium, format: .number)
Of course you would need to change your CoreData model to to be of type Double but I think this should have been a Double in the first place.

value being constant in swiftUI

I am sorry im not sure that the title makes sense but if u read, im sure u will understand my problem. I have declared a variable #State var timetext: Int32 in the file CreatingWorkout, with a textfield TextField("5000, 100 etc", value: $timetext, formatter: NumberFormatter()) When i go to the createWorkoutView file and try to present it with the sheet, it wants me to give a value to timetext. However, when i provide a value with the textfield it stays constantly value given when calling with the sheet. I will attach a video here for you to see.
CreatingWorkout.swift :
struct CreatingWorkout: View {
#State var workoutTitle: String
#State var desc: String
#State var timetext: Int32
#State private var iconColor = Color.black
#State var displayWorkout: String = ""
#Environment(\.dismiss) var dismiss
#Environment(\.managedObjectContext) private var viewContext
private func saveWorkout() {
do {
let workout = Workout(context: viewContext)
workout.title = workoutTitle
workout.time = timetext
workout.icon = displayWorkout
workout.descriptionn = desc
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
CreateWorkout.swift :
import SwiftUI
struct CreateWorkoutView: View {
#State private var showingCreateWorkout = false
#State var timetext: Int32 = 0
var body: some View {
NavigationView {
VStack {
VStack(alignment: .center, spacing: 20) {
Text("Fitzy")
.font(.largeTitle)
.fontWeight(.bold)
Text("Create your first Workout")
.font(.title3)
.foregroundColor(.gray)
Button {
showingCreateWorkout.toggle()
} label: {
Image(systemName: "plus")
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(Color("AccentColor"))
.frame(width: 50, height: 50)
)
}.sheet(isPresented: $showingCreateWorkout) {
CreatingWorkout(workoutTitle: "", desc: "", timetext: timetext)
}
}
}.navigationTitle("Create Workout")
}
}
}
struct CreateWorkoutView_Previews: PreviewProvider {
static var previews: some View {
CreateWorkoutView()
}
}
https://drive.google.com/file/d/1qQDQmap5bMz9LxzibV98epHW5urqUtZ7/view?usp=sharing
as mentioned in the comments, you need a #State property to pass the value
that you type in your TextField to the sheet with CreatingWorkout. Try something like this:
struct CreatingWorkout: View {
#State var workoutTitle: String
#State var desc: String
#State var timetext: Int32
// ....
var body: some View {
Text("\(timetext)")
}
}
struct CreateWorkout: View {
#State var showingCreateWorkout = false
#State var timetext: Int32 = 0 // <-- here
var body: some View {
VStack {
TextField("type a number", value: $timetext, format: .number).border(.red) // <-- here
Button {
showingCreateWorkout.toggle()
} label: {
Image(systemName: "plus").font(.largeTitle).foregroundColor(.red).padding()
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(Color("AccentColor"))
.frame(width: 50, height: 50)
)
}.sheet(isPresented: $showingCreateWorkout) {
CreatingWorkout(workoutTitle: "", desc: "", timetext: timetext) // <-- here
}
}
}
}

Swift UI | Textfield not reading entered value

I have a textfield which is supposed to log the units of a food product someone has eaten, which is then used to calculate the total number of calories, protein, etc. that the user consumed. But when the value is entered on the textfield, the units variable isn't updated. How can I fix this?
This is my code:
#State var selectFood = 0
#State var units = 0
#State var quantity = 1.0
#State var caloriesInput = 0.0
#State var proteinInput = 0.0
#State var carbsInput = 0.0
#State var fatsInput = 0.0
var body: some View {
VStack {
Group {
Picker(selection: $selectFood, label: Text("What did you eat?")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white))
{
ForEach(database.productList.indices, id: \.self) { i in
Text(database.productList[i].name)
}
}
.pickerStyle(MenuPickerStyle())
Spacer(minLength: 25)
Text("How much did you have?")
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(alignment: .leading)
//Textfield not working.
TextField("Units", value: $units, formatter: NumberFormatter())
.padding(10)
.background(Color("Settings"))
.cornerRadius(10)
.foregroundColor(Color("Background"))
.keyboardType(.numberPad)
Button (action: {
self.quantity = ((database.productList[selectFood].weight) * Double(self.units)) / 100
caloriesInput = database.productList[selectFood].calories * quantity
proteinInput = database.productList[selectFood].protein * quantity
carbsInput = database.productList[selectFood].carbs * quantity
fatsInput = database.productList[selectFood].fats * quantity
UIApplication.shared.hideKeyboard()
}) {
ZStack {
Rectangle()
.frame(width: 90, height: 40, alignment: .center)
.background(Color(.black))
.opacity(0.20)
.cornerRadius(15)
;
Text("Enter")
.foregroundColor(.white)
.fontWeight(.bold)
}
}
}
}
}
This is an issue with NumberFormatter that has been going on for a while. If you remove the formatter it updates correctly.
This is a workaround. Sadly it requires 2 variables.
import SwiftUI
struct TFConnection: View {
#State var unitsD: Double = 0
#State var unitsS = ""
var body: some View {
VStack{
//value does not get extracted properly
TextField("units", text: Binding<String>(
get: { unitsS },
set: {
if let value = NumberFormatter().number(from: $0) {
print("valid value")
self.unitsD = value.doubleValue
}else{
unitsS = $0
//Remove the invalid character it is not flawless the user can move to in-between the string
unitsS.removeLast()
print(unitsS)
}
}))
Button("enter"){
print("enter action")
print(unitsD.description)
}
}
}
}
struct TFConnection_Previews: PreviewProvider {
static var previews: some View {
TFConnection()
}
}

How to set textfield character limit SwiftUI?

I'm using SwiftUi version 2 for my application development. I'm facing issue with textfield available in SwiftUI. I don't want to use UITextField anymore. I want to limit the number of Characters in TextField. I searched a lot and i find some answer related to this but those answer doesn't work for SwiftUI version 2.
class textBindingManager: ObservableObject{
let characterLimit: Int
#Published var phoneNumber = "" {
didSet {
if phoneNumber.count > characterLimit && oldValue.count <= characterLimit {
phoneNumber = oldValue
}
}
}
init(limit: Int = 10) {
characterLimit = limit
}
}
struct ContentView: View {
#ObservedObject var textBindingManager = TextBindingManager(limit: 5)
var body: some View {
TextField("Placeholder", text: $textBindingManager.phoneNumber)
}
}
No need to use didSet on your published property. You can add a modifier to TextField and limit the string value to its prefix limited to the character limit:
import SwiftUI
struct ContentView: View {
#ObservedObject var textBindingManager = TextBindingManager(limit: 5)
var body: some View {
TextField("Placeholder", text: $textBindingManager.phoneNumber)
.padding()
.onChange(of: textBindingManager.phoneNumber, perform: editingChanged)
}
func editingChanged(_ value: String) {
textBindingManager.phoneNumber = String(value.prefix(textBindingManager.characterLimit))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class TextBindingManager: ObservableObject {
let characterLimit: Int
#Published var phoneNumber = ""
init(limit: Int = 10){
characterLimit = limit
}
}
The following should be the simpliest. It limits the number of characters to 10.
struct ContentView: View {
#State var searchKey: String = ""
var body: some View {
TextField("Enter text", text: $searchKey)
.onChange(of: searchKey) { newValue in
if newValue.count > 10 {
self.searchKey = String(newValue.prefix(10))
}
}
}
}
This solution wraps everything up in a new Component. You could adapt this to perform other parsing / pattern checking quite easily.
struct ContentView : View {
#State private var myTextValue: String = ""
var body: some View {
LimitedTextField(value: $myTextValue, charLimit: 2)
}
}
struct LimitedTextField : View {
#State private var enteredString: String = ""
#Binding var underlyingString: String
let charLimit : Int
init(value: Binding<String>, charLimit: Int) {
_underlyingString = value
self.charLimit = charLimit
}
var body: some View {
HStack {
TextField("", text: $enteredString, onCommit: updateUnderlyingValue)
.onAppear(perform: { updateEnteredString(newUnderlyingString: underlyingString) })
.onChange(of: enteredString, perform: updateUndelyingString)
.onChange(of: underlyingString, perform: updateEnteredString)
}
}
func updateEnteredString(newUnderlyingString: String) {
enteredString = String(newUnderlyingString.prefix(charLimit))
}
func updateUndelyingString(newEnteredString: String) {
if newEnteredString.count > charLimit {
self.enteredString = String(newEnteredString.prefix(charLimit))
underlyingString = self.enteredString
}
}
func updateUnderlyingValue() {
underlyingString = enteredString
}
}

Make iOS SwiftUI TextField reject bad numeric input

My iOS SwiftUI TextField allows the user to input an Int. If the user types bad input characters such as "abc" instead of "123", I would like to display the bad characters in an error message in my Text (where I wrote textField.text), and I would like to keep the TextField's keyboard on the screen until the user provides correct input. How to make this happen? Thank you in advance. Xcode 11.6 on macOS Catalina 10.15.6.
struct ContentView: View {
#State private var value: Int? = nil;
#State private var text: String = "";
var body: some View {
VStack {
TextField(
"Enter an integer:",
value: $value,
formatter: NumberFormatter(),
onCommit: {
guard let value: Int = self.value else {
self.text = "Bad input \"\(textField.text)\".";
//Do not dismiss the TextField's keyboard.
return;
}
//Dismiss the TextField's keyboard.
self.text = "The product is \(2 * value).";
}
)
.keyboardType(.numbersAndPunctuation)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text(text)
}
.padding([.leading, .trailing], 16)
}
}
Below code gives you an idea for validating your text inside the textfield while user is typing.
struct ContentView: View {
#State private var textNumber : String = ""
#State private var isNumberValid : Bool = true
var body: some View {
VStack {
TextField("Enter an integer:", text: numberValidator())
.keyboardType(.numbersAndPunctuation)
.textFieldStyle(RoundedBorderTextFieldStyle())
if !self.isNumberValid {
Text("Bad input \"\(textNumber)\".")
.font(.callout)
.foregroundColor(Color.red)
}
}
.padding([.leading, .trailing], 16)
}
private func numberValidator() -> Binding<String> {
return Binding<String>(
get: {
return self.textNumber
}) {
if CharacterSet(charactersIn: "1234567890").isSuperset(of: CharacterSet(charactersIn: $0)) {
self.textNumber = $0
self.isNumberValid = true
} else {
self.textNumber = $0
//self.textNumber = ""
self.isNumberValid = false
}
}
}
}
The following code snippet gives you an idea to validate your text inside the text field as the user is typing using Get, Set value
let checkValue = Binding<String>(
get: {
self.value
},
set: {
self.value = $0
}
)
I hope it can be of help to you
struct ContentView: View {
#State private var value: String = ""
#State private var text: String = ""
var body: some View {
let checkValue = Binding<String>(
get: {
self.value
},
set: {
self.value = $0
}
)
return VStack {
TextField("Enter an integer:",text: checkValue)
.keyboardType(.numbersAndPunctuation)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("Bad interger: \(Int(self.value) != nil ? "" : self.value)").foregroundColor(Color.red)
}
.padding([.leading, .trailing], 16)
}
}

Resources