How do I get to the proper value of the the amount entered in textfield? Assuming my dollar value is 50.05, I noticed that when I try to access:
bindingManager.text.decimal
I get 5005. What am I doing wrong to not get 50.05?
import SwiftUI
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ContentView: View {
#ObservedObject private var bindingManager = TextBindingManager(amount: 0)
var decimal: Decimal { bindingManager.text.decimal / pow(10, Formatter.currency.maximumFractionDigits) }
var maximum: Decimal = 999_999_999.99
#State private var lastValue: String = ""
#State private var locale: Locale = .current {
didSet { Formatter.currency.locale = locale }
}
var body: some View {
VStack(alignment: .leading) {
TextField(bindingManager.text, text: $bindingManager.text)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing) // this will keep the text aligned to the right
.onChange(of: bindingManager.text) { string in
if string.decimal > maximum {
self.bindingManager.text = lastValue
} else {
self.bindingManager.text = decimal.currency
lastValue = self.bindingManager.text
}
return
}
}
.padding()
.onAppear {
Formatter.currency.locale = locale
}
}
}
class TextBindingManager: ObservableObject {
#Published var text: String = ""
var amount: Decimal = .zero
init(amount: Decimal) {
self.amount = amount
self.text = Formatter.currency.string(for: amount) ?? "$0.00"
}
}
fileprivate extension Formatter {
static let currency: NumberFormatter = .init(numberStyle: .currency)
}
extension NumberFormatter {
convenience init(numberStyle: Style) {
self.init()
self.numberStyle = numberStyle
}
}
extension StringProtocol where Self: RangeReplaceableCollection {
var digits: Self { filter (\.isWholeNumber) }
}
extension String {
var decimal: Decimal { Decimal(string: digits) ?? 0 }
}
extension Decimal {
var currency: String { Formatter.currency.string(for: self) ?? "" }
}
You just need to divide the decimal value by the number of maximum fraction digits. Same as it is being done with the decimal instance property of your ContentView:
var decimal: Decimal { bindingManager.text.decimal / pow(10, Formatter.currency.maximumFractionDigits) }
.onChange(of: bindingManager.text) { string in
if string.decimal > maximum {
self.bindingManager.text = lastValue
} else {
self.bindingManager.text = decimal.currency
lastValue = self.bindingManager.text
}
print("decimal", decimal)
return
}
This will print
decimal 0.05
decimal 0.5
decimal 5
decimal 50.05
Related
I tried to build a NumberField from a TextField where the value is validated and pushed along the Binding only when the .onSubmit modifier is called. The logic seems to work fine but you will see that the NumberField is not updated properly when the submitted value is outside the specified range.
Here's my code for the View:
struct NumberField: View {
init(
value: Binding<Double>,
range: ClosedRange<Double>
) {
self._value = value
self.range = range
self._valueStore = State(initialValue: value.wrappedValue)
}
#Binding var value: Double
let range: ClosedRange<Double>
#State private var valueStore: Double
var body: some View {
TextField(
"",
value: .init(
get: {
valueStore
},
set: { newValue in
valueStore = validate(
newValue,
range: range
)
}),
formatter: NumberFormatter()
)
.onSubmit {
value = valueStore
}
.onChange(of: value) { newValue in
guard newValue != valueStore else {
return
}
valueStore = newValue
}
}
func validate(_ newValue: Double, range: ClosedRange<Double>) -> Double {
let validatedValue = clamp(newValue, to: range)
print("validate - newValue: \(newValue) - validated: \(validatedValue)")
return validatedValue
}
func clamp(_ value: Double, to range: ClosedRange<Double>) -> Double {
min(max(range.lowerBound, value), range.upperBound)
}
}
You can use it like so in a playground (or a macOS app):
struct MainView: View {
#State var value = 2.0
var body: some View {
VStack {
Text("value: \(value)")
NumberField(value: $value, range: 2.0 ... 10)
NumberField(value: $value, range: 2.0 ... 10)
}
.frame(width: 200)
.padding()
}
}
I am a beginner developer and I want to add my price variable which is a double next to the title variable in my view. When i try Text(price) is is giving me the error "No exact matches in call to initializer". Is this because I cannot use a double inside a Text?
import SwiftUI
struct TaskRow: View {
var task: String
var price: Double
var completed: Bool
var body: some View {
HStack(spacing: 20) {
Image(systemName: completed ?
"checkmark.circle" : "circle")
Text(price) "No exact matches in call to initializer"
Text(task)
}
}
}
struct TaskRow_Previews: PreviewProvider {
static var previews: some View {
TaskRow(task: "Do laundry", price: 1.00, completed: true)
}
}
Screenshot of issue:
The issue is Text takes in a String, so you just need to convert the Double to a String first. You can do this if you want to plan for locale or different currencies
import SwiftUI
struct TaskRow: View {
var task: String
var price: Double
var completed: Bool
var priceString: String {
return price.toLocalCurrency()
}
var body: some View {
HStack(spacing: 20) {
Image(systemName: completed ?
"checkmark.circle" : "circle")
Text(priceString)
Text(task)
}
}
}
struct TaskRow_Previews: PreviewProvider {
static var previews: some View {
TaskRow(task: "Do laundry", price: 1.00, completed: true)
}
}
// This could be in another file
// Extension to convert doubles to currency strings consistently
extension Double {
func toLocalCurrency() -> String {
let formatter = NumberFormatter()
var priceString = self.description
formatter.locale = Locale.current
formatter.numberStyle = .currency
if let formattedTipAmount = formatter.string(from: NSNumber(value: self)) {
priceString = formattedTipAmount
}
return priceString
}
}
You could also use any of these if you don't want to bother with Locale for now:
Text("\(price)") // 1.00
Text(price.description) // 1.00
Text("$\(price)") // $1.00
I have the following code:
struct Food {
var name: String
var energy: Float = 0
var water: Float = 0
}
struct FoodItemView: View {
#State var newFoodItem = Food(name: "")
private var numberFormatter = NumberFormatter()
var body: some View {
Form {
Section(header: Text("Basics").fontWeight(.bold)) {
TextField("Name", text: $newFoodItem.name)
TextField("Energy (C)", value: $newFoodItem.energy, formatter: numberFormatter)
.keyboardType(.numberPad)
TextField("Water (g)", value: $newFoodItem.water, formatter: numberFormatter)
}
}
}
How do I make it so that, the value displayed in my TextFields would be an empty string if the bound value is 0, rather than explicitly displaying the 0?
Set .zeroSymbol property of NumberFormatter.
struct FoodItemView: View {
#State var newFoodItem = Food(name: "")
private var numberFormatter = NumberFormatter()
init() {
numberFormatter.zeroSymbol = "" //<< Here
}
// Other Code
I recently started learning SwiftUI and I want to make a unit converter. In the process I ran into a problem: it is necessary to make sure that when a numerical value is entered into one of the TextField, the rest of the TextField formulas are triggered and the total values are displayed.
import SwiftUI
import Combine
struct ContentView: View {
#State var celsius: String = ""
#State var kelvin: String = ""
#State var farenheit: String = ""
#State var reyumur: String = ""
#State var rankin: String = ""
var body: some View {
NavigationView {
Temperature(celsius: $celsius, kelvin: $kelvin, farenheit: $farenheit, reyumur: $reyumur, rankin: $rankin)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Temperature: View {
#Binding var celsius: String
#Binding var kelvin: String
#Binding var farenheit: String
#Binding var reyumur: String
#Binding var rankin: String
var body: some View {
List {
Section(
header: Text("Международная система (СИ)")) {
HStack {
TextField("Enter value", text: $celsius)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(celsius)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.celsius = filtered
}
}
Text("°C")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
HStack {
TextField("Enter value", text: $kelvin)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(kelvin)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.kelvin = filtered
}
}
Text("K")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
}
Section(
header: Text("США и Британия")) {
HStack {
TextField("Enter value" , text: $farenheit)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(farenheit)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.farenheit = filtered
}
}
Text("F")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
}
Section(
header: Text("Редкоиспользуемые")) {
HStack {
TextField("Enter value" , text: $reyumur)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(reyumur)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.reyumur = filtered
}
}
Text("Re")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
HStack {
TextField("Enter value" , text: $rankin)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(rankin)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.rankin = filtered
}
}
Text("R")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
}
}
.navigationBarTitle("Temperature")
.navigationBarTitleDisplayMode(.inline)
}
}
With SwiftUI 3, #FocusState can be used to detect if TextField is focused, which enables doing different works according to its value. You could set one variable to be the main, and other values should be calculated from it. For each value, calculate in the forward and reverse direction with two functions:
struct CelsiusAndKelvin: View {
#State private var celsius: String = "" // Assume all other values are based on celsius
#FocusState private var isKelvinFocused: Bool // When focused,
#State private var kelvinWhenFocused: String? // you may not want it to be calculated from other values
private var kelvinMask: Binding<String> {
Binding { // If `isKelvinFocused` turns true, this getter will be called first
if isKelvinFocused && kelvinWhenFocused != nil {
return kelvinWhenFocused!
}
if let x = Double(celsius) {
return String(format: "%.2f", c2k(x))
}
return ""
} set: {
if isKelvinFocused { kelvinWhenFocused = $0 }
if let x = Double($0) {
celsius = String(format: "%.2f", k2c(x))
} else {
celsius = ""
}
}
}
var body: some View {
Form {
TextField("°C", text: $celsius)
TextField("K", text: kelvinMask)
.focused($isKelvinFocused)
}
.onChange(of: isKelvinFocused) {
// Make `isKelvinFocused` always equal to `kelvinWhenFocused != nil`
if !$0 { kelvinWhenFocused = nil }
}
}
}
extension CelsiusAndKelvin {
private func c2k(_ x: Double) -> Double {
return x + 273.15
}
private func k2c(_ x: Double) -> Double {
return x - 273.15
}
}
I have the following form:
struct RentView: View {
#ObservedObject private var viewModel: RentViewModel
init(viewModel: RentViewModel){
self.viewModel = viewModel
}
var textField : TextField
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter total rent")
.fontWeight(.bold)) {
TextField("Total rent", text: $viewModel.amount.totalRent)
.keyboardType(.decimalPad)
}
Section(header: Text("Enter total bills?")
.fontWeight(.bold)) {
TextField("Total bills", text: $viewModel.amount.totalBills)
.keyboardType(.decimalPad)
}
Section(header: Text("Enter wages?")
.fontWeight(.bold)) {
TextField("Your monthly wage", text: $viewModel.amount.myMonthlyIncome)
.keyboardType(.decimalPad)
}
Section(header: Text("What are the wages of your housemates?")
.fontWeight(.bold)) {
ForEach(viewModel.incomes.indices){
index in TextField("Housemate \(index + 1)", text: $viewModel.incomes[index])
.keyboardType(.decimalPad)
}
}
Section {
Text("Your share: £\(viewModel.yourShare, specifier: "%.2f")")
.fontWeight(.bold)
.font(Font.system(size: 22, design: .default))
.font(.subheadline)
}.foregroundColor(Color.red)
if textField != nil && !textField.hasText {
Text("Please fill in all sections")
}
}
}
}
}
This calculates the rent equally between housemates based on income. However it will only update if all of the forms are filled in. Therefore I want a message to pop up saying "Please fill in all sections" (as above in last section) if one text field has been left empty. However I am getting the error: "Reference to generic type 'TextField' requires arguments in <...>" I am very new to Swift and am not sure what arguments are required here and why?
My Rent View Model looks like this:
import Foundation
import Combine
class RentViewModel : ObservableObject {
#Published var amount: Amounts
#Published var incomes: [String]
init(_ amount: Amounts, housemates: Int){
self.amount = amount
self.incomes = Array(repeating: "", count: housemates)
}
var myMonthlyIncome : String { return amount.myMonthlyIncome }
var housemateMonthlyIncome : String { return amount.housemateMonthlyIncome }
var totalRent : String { return amount.totalRent }
var totalBills : String { return amount.totalBills }
var yourShare: Double {
guard let totalRent = Double(totalRent) else { return 0 }
guard let totalBills = Double(totalBills) else {return 0 }
guard let myMonthlyIncome = Double(myMonthlyIncome) else { return 0 }
let incomesInt = incomes.compactMap { Int($0) }
let housemateMonthlyIncome = Double(incomesInt.reduce(0, +))
let totalIncome = Double(myMonthlyIncome + housemateMonthlyIncome)
let percentage = myMonthlyIncome / totalIncome
let householdTotal = totalRent + totalBills
let value = (householdTotal * percentage)
return (round(100*value)/100)
}
}