Swiftui TextField with Formatter error with numeric value - swiftui

I have this simply code, that work fine if I digit only numbers or one point:
import SwiftUI
struct ContentView: View {
#State private var doublevalue: Double?
private var doubleformatter: NumberFormatter {
let f = NumberFormatter()
f.numberStyle = .decimal
f.minimumFractionDigits = 5
f.maximumFractionDigits = 5
return f
}
var body: some View {
TextField("0.00", value: $doublevalue, formatter: doubleformatter)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
if I digit letter I have this error:
How to remove this error?

Related

When optional starts as nil, changing value doesn't work

I'm using MVVM with Swift UI. I have the following Struct, ViewModel and View
struct Thing: Identifiable, Codable, Hashable {
var id: String = UUID().uuidString
var price: Double?
}
class MyViewModel: ObservableObject {
//****
#Published var things : [Thing] = [Thing(id: "abc1234")]{
didSet{
print(things)
}
}
}
struct MyView: View {
#StateObject var myViewModel: MyViewModel = MyViewModel()
private let numberFormatter: NumberFormatter
init() {
numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
numberFormatter.maximumFractionDigits = 2
}
var body: some View {
VStack{
List{
ForEach(Array($myViewModel.things.enumerated()), id: \.offset) { index, $thing in
HStack{
//AnotherView(thing: $thing)
TextField("$0.00", value: $myViewModel.things[index].price, formatter: numberFormatter)
.keyboardType(.numberPad)
}//HStack
}//ForEach
}//List
}//VStack
}//View
}
With the above code, anytime the textfield is changed the didSet print statement will show that price = nil
However if I change the line under the comment with the ***** to the following, initializing price to 0 the changes in the textfield are correctly written back to the [Thing] array and it prints that its Optional(x.xx)
#Published var things : [Thing] = [Thing(id: "abc1234", price: 0)]{
What I also just figured out is that if you use the above line with price initialized to 0, if you backspace the default $0.00 in the TextField, it sets the value back to nil, and then it never changes again.
Price should not be optional just default it to 0. There is also a mistake in the ForEach View (it is not a for loop it needs to be given an identifiable array), fix as follows:
ForEach($store.things){ $thing in
HStack{
//AnotherView(thing: $thing)
TextField("$0.00", value: $thing.price, formatter: NumberFormatter.myCurrencyFormatter)
.keyboardType(.numberPad)
}//HStack
Note the formatter needs to be a singleton, global or static because we shouldn't init objects in View init or body.

How to separate input values for each Textfield?

I stored the user's input into a dictionary, but the variables name and amount seems to not be a separate value for each Textfield rows. I tried adding self. to name and amount, but that seemed to not do anything. How can I implement this?
#Binding var numPeople: Int
#State var dict: [String : Float] = [:]
#State var name: String = ""
#State var amount: Float = 0.00
.
.
.
ForEach(1...numPeople, id:\.self) { stack in
HStack {
TextField("Name", text: $name)
.padding()
Text("Amount in $:")
TextField("", value: $amount, formatter: NumberFormatter())
.keyboardType(.numberPad)
.onReceive(Just(amount)) { _ in
dict[name] = amount
}
.padding()
}
}
Thank you!
In your code you are using the same variables name and amount for all rows that you iterate with ForEach. If you want to have each row with their own fields managed separately, you need to separate the views.
Here below, a very schematic example of how it works:
In the parent view, the ForEach will call a subview:
#Binding var numPeople: Int
// Make #State vars private
#State private var dict: [String : Float] = [:]
// Note that you don't use the variables name and amount here
.
.
.
ForEach(1...numPeople, id:\.self) { stack in
// Pass the dictionary, it will be updated by the subview
SubView(dict: $dict)
}
Create a subview that will separately manage each name/ amount:
struct SubView: View {
#Binding var dict: [String : Float]
#State private var name: String = ""
#State private var amount: Float = 0.00
var body: some View {
HStack {
TextField("Name", text: $name)
.padding()
Text("Amount in $:")
TextField("", value: $amount, formatter: NumberFormatter())
.keyboardType(.numberPad)
// I don't know why you need this, if the amount is
// updated in this view. Maybe you can just use
// dict[name] = amount, dropping the .onReceive()...
// ... but it depends on your code
.onReceive(Just(amount)) { _ in
dict[name] = amount
}
.padding()
}
}
private func whatToDoWithNameAndAmount() {
// Do whatever else you need with these variables
}
}

How should I get and set a value of UserDefaults?

I'm currently developing an application using SwiftUI.
I want to use a UserDefaults value in this app.
So I made a code below.
But in this case, when I reboot the app(the 4'th process in the process below), I can't get value from UserDefaults...
Build and Run this project.
Pless the home button and the app goes to the background.
Double-tap the home button and remove the app screen.
press the app icon and reboot the app. Then I want to get value from UserDefaults.
to resolve this problem how should I set and get a value in UserDefaults?
Here is the code:
import SwiftUI
struct ContentView: View {
#State var text = "initialText"
var body: some View {
VStack {
Text(text)
TextField( "", text: $text)
}.onAppear(){
if let text = UserDefaults.standard.object(forKey: "text" ){
self.text = text as! String
}
}
.onDisappear(){
UserDefaults.standard.set(self.text, forKey: "text")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ADD
When I add this class following the first answer, that code has a couple of errors like this, is it usual?
Xcode: Version 11.7
Swift: Swift 5
Set in a class like this your values: Bool, String(see example), Int, etc...
#if os(iOS)
import UIKit
#else
import AppKit
#endif
import Combine
#propertyWrapper struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
final class UserSettings: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#UserDefault("myText", defaultValue: "initialText")
var myText: String {
willSet { objectWillChange.send() }
}
}
this to read:
let settings = UserSettings()
let count = settings.countSentence // default countsentence 1
this to update:
let settings = UserSettings()
settings.countSentence = 3 // default countsentence 3
Based on your code:
struct ContentView: View {
let UserDef = UserSettings()
#State var text = ""
var body: some View {
VStack {
Text(UserDef.myText)
TextField("placeholder", text: $text, onCommit: { self.UserDef.myText = self.text})
}.onAppear() {
self.text = self.UserDef.myText
}
}
}

SwiftUI pass a computed variable to next view

I'm new to IOS and SwiftUI coding. I googled a lot but could not find a solution, how to pass a computed variable to the next view.
Here snippets of what I have:
import SwiftUI
struct ContentView: View {
#State private var isShowingResultView = false
#State private var netRate = "0"
#State var daysMonth = "0"
#State var hoursWeek: String = "0"
#State var daysOnsite: String = "0"
#State var ticketCost: String = "0"
#State var hotelCost: String = "0"
#State var otherCost: String = "0"
//#State var travellCostResult: Double = 0.00
var travellCostPerHour: Double{
get {
let daysMonthNbr = Int(daysMonth) ?? 0
let hoursWeekNbr = Int(hoursWeek) ?? 0
let daysOnsiteNbr = Int(daysOnsite) ?? 0
let ticketCostNbr = Double(ticketCost) ?? 0
let hotelCostNbr = Double(hotelCost) ?? 0
let otherCostNbr = Double(otherCost) ?? 0
let travellCostPerWeek = (ticketCostNbr + (Double((daysOnsiteNbr-1))*hotelCostNbr)+otherCostNbr)
let travellCostPerHour: Double = Double(travellCostPerWeek) / Double(hoursWeekNbr)
return travellCostPerHour.isNaN ? 0 : travellCostPerHour
}
}
.
.
.
var body: some View {
HStack {
NavigationLink("Calculate", destination: ResultView(netRate: self.$netRate, travellCostPerHour: travellCostPerHour), isActive: $isShowingResultView).navigationBarTitle("Result").buttonStyle(GradientButtonStyle())
.adaptToKeyboard()
}
struct ResultView: View {
#Binding var netRate: String
#Binding var travellCostPerHour: Double
.
.
.
struct ResultView_Previews: PreviewProvider {
#State static var netRate: String = ""
#State static var travellCostPerHour: Double = 0.00
static var previews: some View {
ResultView(netRate: $netRate, travellCostPerHour: $travellCostPerHour )
}
}
I get this error msg in the navigationLink for travellCostPerHour: Cannot convert value of type 'Double' to expected argument type 'Binding'
Can one put me on the right path here please?
If it is computed property then binding is not needed, pass it as-is
struct ResultView: View {
#Binding var netRate: String
var travellCostPerHour: Double // << just regular
// .. other code

SwiftUI #State var initialization issue

I would like to initialise the value of a #State var in SwiftUI through the init() method of a Struct, so it can take the proper text from a prepared dictionary for manipulation purposes in a TextField.
The source code looks like this:
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
TextField($fullText)
}
}
Unfortunately the execution fails with the error Thread 1: Fatal error: Accessing State<String> outside View.body
How can I resolve the situation? Thank you very much in advance!
SwiftUI doesn't allow you to change #State in the initializer but you can initialize it.
Remove the default value and use _fullText to set #State directly instead of going through the property wrapper accessor.
#State var fullText: String // No default value of ""
init(letter: String) {
_fullText = State(initialValue: list[letter]!)
}
I would try to initialise it in onAppear.
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
var body: some View {
TextField($fullText)
.onAppear {
self.fullText = list[letter]!
}
}
}
Or, even better, use a model object (a BindableObject linked to your view) and do all the initialisation and business logic there. Your view will update to reflect the changes automatically.
Update: BindableObject is now called ObservableObject.
The top answer is incorrect. One should never use State(initialValue:) or State(wrappedValue:) to initialize state in a View's init. In fact, State should only be initialized inline, like so:
#State private var fullText: String = "The value"
If that's not feasible, use #Binding, #ObservedObject, a combination between #Binding and #State or even a custom DynamicProperty
In your specific case, #Bindable + #State + onAppear + onChange should do the trick.
More about this and in general how DynamicPropertys work, here.
It's not an issue nowadays to set a default value of the #State variables inside the init method. But you MUST just get rid of the default value which you gave to the state and it will work as desired:
,,,
#State var fullText: String // <- No default value here
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
TextField("", text: $fullText)
}
}
Depending on the case, you can initialize the State in different ways:
// With default value
#State var fullText: String = "XXX"
// Not optional value and without default value
#State var fullText: String
init(x: String) {
fullText = x
}
// Optional value and without default value
#State var fullText: String
init(x: String) {
_fullText = State(initialValue: x)
}
The answer of Bogdan Farca is right for this case but we can't say this is the solution for the asked question because I found there is the issue with the Textfield in the asked question. Still we can use the init for the same code So look into the below code it shows the exact solution for asked question.
struct StateFromOutside: View {
let list = [
"a": "Letter A",
"b": "Letter B",
// ...
]
#State var fullText: String = ""
init(letter: String) {
self.fullText = list[letter]!
}
var body: some View {
VStack {
Text("\(self.fullText)")
TextField("Enter some text", text: $fullText)
}
}
}
And use this by simply calling inside your view
struct ContentView: View {
var body: some View {
StateFromOutside(letter: "a")
}
}
You can create a view model and initiate the same as well :
class LetterViewModel: ObservableObject {
var fullText: String
let listTemp = [
"a": "Letter A",
"b": "Letter B",
// ...
]
init(initialLetter: String) {
fullText = listTemp[initialLetter] ?? ""
}
}
struct LetterView: View {
#State var viewmodel: LetterViewModel
var body: some View {
TextField("Enter text", text: $viewmodel.fullText)
}
}
And then call the view like this:
struct ContentView: View {
var body: some View {
LetterView(viewmodel: LetterViewModel(initialLetter: "a"))
}
}
By this you would also not have to call the State instantiate method.
See the .id(count) in the example come below.
import SwiftUI
import MapKit
struct ContentView: View {
#State private var count = 0
var body: some View {
Button("Tap me") {
self.count += 1
print(count)
}
Spacer()
testView(count: count).id(count) // <------ THIS IS IMPORTANT. Without this "id" the initializer setting affects the testView only once and calling testView again won't change it (not desirable, of course)
}
}
struct testView: View {
var count2: Int
#State private var region: MKCoordinateRegion
init(count: Int) {
count2 = 2*count
print("in testView: \(count)")
let lon = -0.1246402 + Double(count) / 100.0
let lat = 51.50007773 + Double(count) / 100.0
let myRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: lon) , span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
_region = State(initialValue: myRegion)
}
var body: some View {
Map(coordinateRegion: $region, interactionModes: MapInteractionModes.all)
Text("\(count2)")
}
}