I've learning SwiftUI for a week, recently I found a confusing issue with it.
#State private var checkAmount = 0.0
var body: some View {
NavigationView {
Form {
Section {
TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
.keyboardType(.decimalPad)
}
Section{
Text(checkAmount, format:.currency(code: Locale.current.currency?.identifier ?? "USD"))
}
}
}
}
I use TextFile to receive user's input and alter the value of checkAmount, and make the value shown in the section below. Here is my preview in xcode.
Preview
But when I type a random number and delete it all, this happend:
Still a digit here
It seems SwiftUI didn't delete it all, and still keep the last digit I deleted.
I guess maybe I should give it an default value when user's input is empty?
Some additional information maybe necessary: I'm using a M1 MacMini and XCode 14.1
Approach
For currency(code:) the value type is Decimal, so you have to use Optional Decimal
https://developer.apple.com/documentation/foundation/parseableformatstyle/3796617-currency
When the last digit is deleted it becomes nil
Use checkAmount ?? 0 in your TextField
Code
struct ContentView: View {
#State private var checkAmount = Decimal?(0.0) //Optional decimal
var body: some View {
NavigationView {
Form {
Section {
TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
.keyboardType(.decimalPad)
}
Section{
//Use ?? operator to display 0 when nil
Text(checkAmount ?? 0, format:.currency(code: Locale.current.currency?.identifier ?? "USD"))
}
}
.onChange(of: checkAmount) { newValue in
print("checkAmount = \(String(describing: newValue))")
}
}
}
}
Related
When I have multiple text fields rendered in SwiftUI in a given view, I am getting noticeable lag that is directly proportional to the number of text fields. If I change these to simple text views, the lag goes down considerably.
I have looked at SO and found a few questions about lag with TextField but generally it seems like there's a preponderance that the lag is caused by the data source because when using a constant value, the lag is not observed.
I have created a demo project to illustrate the issue. I have an array of 20 contact names and for each name create a contact card with three email addresses. If I toggle the view between rendering the email addresses as Text vs TextField Views (with a constant value), the time taken from button tap to the last view's .onAppear is 80-100 ms (Text) and 300-320 ms (TextField).
Both views take a noticeable time to render, but clearly the TextFields take a significantly longer time to render on this contrived, trivial app. In our app, we are rendering significantly more information and not using constant values for the TextFields so this lag produces more pronounced effects (sometimes a few seconds). Is there some way around this issue for SwiftUI TextFields? Below is the code for the demo project. I know there are better ways to write the code, just threw it together quickly to demonstrate the speed issues.
Also, interestingly, if I put the ForEach into a List (or just try to use a list directly from the array data), no ContactCard views are rendered at all.
Any help is greatly appreciated!
import SwiftUI
var formatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
return formatter
}
struct ContentView: View {
let contacts: Array<(first: String, last: String)> = [
("John", "Stone"),
("Ponnappa", "Priya"),
("Mia", "Wong"),
("Peter", "Stanbridge"),
("Natalie", "Lee-Walsh"),
("Ang", "Li"),
("Nguta", "Ithya"),
("Tamzyn", "French"),
("Salome", "Simoes"),
("Trevor", "Virtue"),
("Tarryn", "Campbell-Gillies"),
("Eugenia", "Anders"),
("Andrew", "Kazantzis"),
("Verona", "Blair"),
("Jane", "Meldrum"),
(" Maureen", "M. Smith"),
("Desiree", "Burch"),
("Daly", "Harry"),
("Hayman", "Andrews"),
("Ruveni", "Ellawala")
]
#State var isTextField = false
var body: some View {
ScrollView {
VStack {
HStack {
Button("Text") {
print("text tapped: \(formatter.string(from: Date()))")
isTextField = false
}
Button("TextField") {
print("text tapped: \(formatter.string(from: Date()))")
isTextField = true
}
}
ForEach(contacts, id: \.self.last) { contact in
ContactCard(name: contact, isTextField: $isTextField)
}
}
}
}
}
struct ContactCard: View {
var name: (first: String, last: String)
#Binding var isTextField: Bool
var emailAddresses: Array<String> {
[
"\(name.first).\(name.last)#home.com",
"\(name.first).\(name.last)#work.com",
"\(name.first).\(name.last)#work.org",
]
}
var body: some View {
VStack {
Text("\(name.first) \(name.last)")
.font(.headline)
ForEach(emailAddresses, id: \.self) { email in
HStack {
Text("Email")
.frame(width: 100)
if isTextField {
TextField("", text: .constant(email))
.onAppear(){
print("view appeared: \(formatter.string(from: Date()))")
}
} else {
Text(email)
.onAppear(){
print("view appeared: \(formatter.string(from: Date()))")
}
}
Spacer()
}
.font(.body)
}
}
.padding()
}
}
Use LazyVStack in your scroll view instead of VStack. It worked for me, tested using 200 contact names.
I am trying to populate a picker based on the selection of another picker. I am new to Swift and have been beating my head on this for way too long. I am sure its not as difficult as I am making it but I would appreciate any assistance.
I think my biggest issue is passing the selection of the first picker to the array name of the second. I have used switch case, tried to pass the selection raw value...etc. Below is a sample of what I would like it to look like without the binding of the pickers. Thanks
import SwiftUI
struct veggie: View {
let veggies = ["Beans", "Corn", "Potatoes"]
let beanList = ["Pole", "String", "Black"]
let cornList = ["Peaches & Cream", "Sweet"]
let potatoList = ["Yukon Gold", "Idaho"]
#State private var selectedVeggie = "Bean"
#State private var selectedBean = "Pole"
#State private var selectedType = ""
var body: some View {
NavigationView{
VStack{
Form{
Picker("Please choose a veggie", selection: $selectedVeggie)
{
ForEach(veggies, id: \.self) {
Text($0)
}
}
Text("You selected \(selectedVeggie)")
Picker("Type", selection: $selectedBean)
{
ForEach(beanList, id: \.self) {
Text($0)
}
}
}
} .navigationTitle("Veggie Picker")
}
}
}
I'm imagining that you want this to be somewhat dynamic and not hardcoded with if statements. In order to accomplish that, I setup an enum with the veggie types and then a dictionary that maps the veggie types to their subtypes. Those look like:
enum VeggieType : String, CaseIterable {
case beans, corn, potatoes
}
let subTypes : [VeggieType: [String]] = [.beans: ["Pole", "String", "Black"],
.corn: ["Peaches & Cream", "Sweet"],
.potatoes: ["Yukon Gold", "Idaho"]]
Then, in the view, I did this:
struct ContentView: View {
#State private var selectedVeggie : VeggieType = .beans
#State private var selectedSubtype : String?
var subtypeList : [String] {
subTypes[selectedVeggie] ?? []
}
var body: some View {
NavigationView{
VStack{
Form{
Picker("Please choose a veggie", selection: $selectedVeggie)
{
ForEach(VeggieType.allCases, id: \.self) {
Text($0.rawValue.capitalized)
}
}
Text("You selected \(selectedVeggie.rawValue.capitalized)")
Picker("Type", selection: $selectedSubtype)
{
ForEach(subtypeList, id: \.self) {
Text($0).tag($0 as String?)
}
}
}
} .navigationTitle("Veggie Picker")
}
}
}
The selectedVeggie is now typed with VeggieType. The selectedSubtype is an optional String that doesn't have an initial value (although you could set one if you want).
The first picker goes through all of the cases of VeggieType. The second one dynamically changes based on the computed property subtypeList which grabs the item from the subTypes dictionary I had made earlier.
The tag item is important -- I had to make the tag as String? because SwiftUI wants the selection parameter and the tag() to match exactly.
Note: you say you're new to Swift, so I'll mention that you had named your view `veggie` -- in Swift, normally types have capital names. I named mine `ContentView`, but you could rename it `VeggieView` or something like that.
I searched for a good approach for this problem and none of the questions I'v found answered my needs. I'm struggling with this.
What I need is a TextField that would accept only 0123456789. with format of %.2f and wouldn't require to press Return key to submit. Receiving the value as Double and return as Double.
The results should be 123.45 or 1.23.
My attempt:
var title: String
let numberFormatter = NumberFormatter()
#Binding var value: Double
var amountProxy: Binding<String> {
Binding<String>(
get: { String(format: "%.2f", value) },
set: {
if let value = numberFormatter.number(from: $0) {
self.value = value.doubleValue
}
}
)
}
var body: some View{
HStack(spacing: 8){
Text(LocalizedStringKey(title + ":"))
.layoutPriority(1)
TextField(LocalizedStringKey(""), text: amountProxy)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
}
.lineLimit(1)
}
This method results were forcing the user to type one number and move precisely after the decimal point to get the requested value 4.20. Something like type, move, repeat. Very poor UX.
I'm working on MacOS and I try to make NumberField — something like TextField, but for numbers. In rather big tree of views at the top I had:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
TextField("",
text: Binding(
get: {"\(String(format: "%.1f", instances[index].values[valueIndex]))"},
set: {setValueForInstance(index, valueIndex, $0)})
)
}
}
...
And it worked well, but not nice:
✔︎ when I changed value, all View structure was redrawn – good
✔︎ values was updated if they were changed by another part of Views structure – good
✖︎ it was updated after each keypresses, which was annoying, when I tried to input 1.2, just after pressing 1 view was updated to 1.0. Possible to input every number but inconvenient – bad
So, I tried to build NumberField.
var format = "%.1f"
struct NumberField : View {
#Binding var number: Double {
didSet {
stringNumber = String(format: format, number)
}
}
#State var stringNumber: String
var body: some View {
TextField("" , text: $stringNumber, onCommit: {
print ("Commiting")
if let v = Double(stringNumber) {
number = v
} else {
stringNumber = String(format:format, number)
}
print ("\(stringNumber) , \(number)" )
})
}
init (number: Binding<Double>) {
self._number = number
self._stringNumber = State(wrappedValue: String(format:format, number.wrappedValue))
}
}
And It's called from the same place as before:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
NumberField($instances[index].values[valueIndex])
}
}
...
But in this case it never updates NumberField View if values was changed by another part of View. Whats's wrong? Where is a trick?
I have a stepper:
Stepper(value: $timeBetweenMeals, in: 1...10, step: 1) {
Text("Time Between Meals: \(timeBetweenMeals, specifier: "%.0f")h")
self.settingsUserDefaults = $timeBetweenMeals
}
That gives me error because I'm trying to set: self.settingsUserDefaults = $timeBetweenMeals inside that view. How can I run that logic? I don't have a button, I just want to save that #Published value as the user changes it
Use a custom Binding, and inside your set you can store your value:
Stepper(value: Binding<Int>(get: {
self.timeBetweenMeals
}, set: {
print($0) // here you can access your value with $0, store it here
self.timeBetweenMeals = $0
}), in: 1...10, step: 1) {
Text("Time Between Meals: \(timeBetweenMeals)h")
}
You need to perform other actions when Stepper value changes in onIncrement and onDecrement method not inside of the label parameter which is meant to return the View of the Stepper. Here is an example (Note: this is just an example, you can achieve this in many easier ways too):
struct ContentView: View {
#State var timeBetweenMeals: Int
var body: some View {
Stepper(value: Binding<Int>(get: {
self.timeBetweenMeals
}, set: {
print($0)
self.timeBetweenMeals = $0
}), in: 1...10, step: 1) {
Text("Time Between Meals: \(timeBetweenMeals)h")
}
}
}
Note: I've updated to a much better approach. Credits to davidev. Also, your specifier seems to be incorrect.