I'm looking for help inputting currency via a form. I would like to have:
Allow entry of a single decimal / comma currency separator
Limit entry to 2 digits past the decimal
Allow value editing
Limit maximum digits entered
There is a similar question asked 6 years ago at Limiting user input to a valid decimal number in Swift, but all the answers appear to be using viewDidLoad. I don't think that is feasible from within a form entry. If it is feasible, please show me how. Thanks
Form {
...
// Enter entry amount
Section {
TextField("Enter Amount > " + curr, text: $moneyS)
.keyboardType(.decimalPad)
}
// select entry save or cancel
Section {
Button(action: {
self.showingAlert.toggle()
...
Following the idea to achieve this, not covering all your points but I think is enough to procede by yourself:
class Test2Model: ObservableObject {
#Published var currLimit: Double = 4.0
#Published var digitLimit: Int = 2
func getCurr(str: String) -> String{
guard let currInserted = Double(str)
else {
return String(currLimit)
}
if currInserted <= currLimit {
return String(currLimit)
}
return str
}
}
struct Test2View: View {
#ObservedObject private var test2Model = Test2Model()
#State private var moneyS: String = ""
var body: some View {
Form {
// Enter entry amount
Section {
TextField("Enter Amount > " + String(test2Model.currLimit), text: $moneyS)
.keyboardType(.decimalPad)
.onChange(of: moneyS, perform: { value in
guard let decimals = moneyS.components(separatedBy:".").last else {return}
if decimals.count > test2Model.digitLimit {
moneyS.removeLast()
}
})
}
// select entry save or cancel
Section {
Button(action: {
moneyS = test2Model.getCurr(str: moneyS)
}, label: {
Text("Check")
})
}
}
}
}
struct Test2View_Previews: PreviewProvider {
static var previews: some View {
Test2View()
}
}
you could try something like this:
struct ContentView: View {
let maxDigits = 6
let maxDecimals = 2
let allowedCharacters = CharacterSet.decimalDigits.union(CharacterSet(charactersIn: NumberFormatter().decimalSeparator))
#State var money: Double?
#State var moneyText = ""
var body: some View {
VStack (spacing: 30) {
Spacer()
TextField("enter a number", text: $moneyText)
.padding(.horizontal, 20)
.keyboardType(.decimalPad)
.onChange(of: moneyText) {
// to prevent pasting non-valid text
let txt = $0.filter { ".0123456789".contains($0) }
if allowed(txt) {
money = Double(txt)
moneyText = txt
} else {
moneyText = String(txt.dropLast())
}
}
Text("Money value is: \(money ?? 0)")
Spacer()
}
}
func allowed(_ txt: String) -> Bool {
let str = txt.trimmingCharacters(in: .whitespacesAndNewlines)
switch str.components(separatedBy: ".").count - 1 {
case 0:
if str.count > maxDigits {
return false
}
return allowedCharacters.isSuperset(of: CharacterSet(charactersIn: str))
case 1:
if str.starts(with: ".") && (str.count - 1) > maxDecimals {
return false
}
let parts = str.split(separator: ".")
if parts.count == 2 && (parts[1].count > maxDecimals) || (str.count - 1) > maxDigits {
return false
}
return allowedCharacters.isSuperset(of: CharacterSet(charactersIn: str))
default:
return false
}
}
}
Related
I'm trying to implement a passcode view in iOS. I was following the guide here.
I'm trying to improve it a bit so it allows me to create a passcode by enter same passcode twice. I added a "state" property to the #StateObject and want to clear entered passcode after user input the passcode first time.
Here is my current code:
LockScreenModel.swift
====================
import Foundation
class LockScreenModel: ObservableObject {
#Published var pin: String = ""
#Published var showPin = false
#Published var isDisabled = false
#Published var state = LockScreenState.normal
}
enum LockScreenState: String, CaseIterable {
case new
case verify
case normal
case remove
}
====================
LockScreen.swift
====================
import SwiftUI
struct LockScreen: View {
#StateObject var lockScreenModel = LockScreenModel()
let initialState: LockScreenState
var handler: (String, LockScreenState, (Bool) -> Void) -> Void
var body: some View {
VStack(spacing: 40) {
Text(NSLocalizedString("lock.label.\(lockScreenModel.state.rawValue)", comment: "")).font(.title)
ZStack {
pinDots
backgroundField
}
showPinStack
}
.onAppear(perform: {lockScreenModel.state = initialState})
.onDisappear(perform: {
lockScreenModel.pin = ""
lockScreenModel.showPin = false
lockScreenModel.isDisabled = false
lockScreenModel.state = .normal
})
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<6) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: 30, weight: .thin, design: .default))
Spacer()
}
}
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { lockScreenModel.pin }, set: { newValue in
if newValue.last?.isWholeNumber == true {
lockScreenModel.pin = newValue
}
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
.disabled(lockScreenModel.isDisabled)
}
private var showPinStack: some View {
HStack {
Spacer()
if !lockScreenModel.pin.isEmpty {
showPinButton
}
}
.frame(height: 20)
.padding([.trailing])
}
private var showPinButton: some View {
Button(action: {
lockScreenModel.showPin.toggle()
}, label: {
lockScreenModel.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
Image(systemName: "eye.fill").foregroundColor(.primary)
})
}
private func submitPin() {
guard !lockScreenModel.pin.isEmpty else {
lockScreenModel.showPin = false
return
}
if lockScreenModel.pin.count == 6 {
lockScreenModel.isDisabled = true
handler(lockScreenModel.pin, lockScreenModel.state) { isSuccess in
if isSuccess && lockScreenModel.state == .new {
lockScreenModel.state = .verify
lockScreenModel.pin = ""
lockScreenModel.isDisabled = false
} else if !isSuccess {
lockScreenModel.pin = ""
lockScreenModel.isDisabled = false
print("this has to called after showing toast why is the failure")
}
}
}
// this code is never reached under normal circumstances. If the user pastes a text with count higher than the
// max digits, we remove the additional characters and make a recursive call.
if lockScreenModel.pin.count > 6 {
lockScreenModel.pin = String(lockScreenModel.pin.prefix(6))
submitPin()
}
}
private func getImageName(at index: Int) -> String {
if index >= lockScreenModel.pin.count {
return "circle"
}
if lockScreenModel.showPin {
return lockScreenModel.pin.digits[index].numberString + ".circle"
}
return "circle.fill"
}
}
extension String {
var digits: [Int] {
var result = [Int]()
for char in self {
if let number = Int(String(char)) {
result.append(number)
}
}
return result
}
}
extension Int {
var numberString: String {
guard self < 10 else { return "0" }
return String(self)
}
}
====================
The problem is the line lockScreenModel.state = .verify. If I include this line, the passcode TextField won't get cleared, but if I remove this line, the passcode TextField is cleared.
If I add a breakpoint in set method of boundPin, I can see after set pin to empty and state to verify, the set method of boundPin is called with newValue of the old pin which I have no idea why. If I only set pin to empty but don't set state to verify, that set method of boundPin won't get called which confuse me even more. I can't figure out which caused this strange behavior.
I'm sorry if this is a naive question, but I need help getting this form to persist in core data. The variables are declared in the data model as strings. I simply cannot get this to cooperate with me. Also, the var wisconsin: String = "" is there because I can't call this view in my NavigationView without it throwing an error.
import SwiftUI
struct WisconsinToolOld: View {
//Variable
var wisconsin: String = ""
#Environment(\.managedObjectContext) private var viewContext
#State var saveInterval: Int = 5
var rateOptions = ["<12", ">12"]
#State var rate = ""
var body: some View {
List {
Section(header: Text("Spontaneous Respirations after 10 Minutes")) {
HStack {
Text("Respiratory Rate")
Spacer()
Picker("Rate", selection: $rate, content: {
ForEach(rateOptions, id: \.self, content: { rate in
Text(rate)
})
})
.pickerStyle(.segmented)
}
Section(header: Text("Result")) {
HStack {
Text("Raw Points")
Spacer()
Text("\(WisconsinToolInterpretation())")
}
}.navigationTitle("Wisconsin Tool")
}
}
func saveTool() {
do {
let wisconsin = Wisconsin(context: viewContext)
wisconsin.rate = rate
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
func WisconsinToolInterpretation() -> Int {
var points = 0
if rate == "<12" {
points += 3
}
else {
points += 1
}
return points
}
}
I know the below is incorrect, and I feel dumb that i can't solve this, but I'm struggling to undertand where to put the 'logic' (and if statment or other) for handeling calculations in my app, based on the entered value and the state of a segmented picker. I can't add it as part of the picker, and it throws an error if i tey and make totalTime a computed property like the below.
I created a quick example to make it simpler to undertand my question. I want to hold the value for use on another screen as well.
struct ContentView: View {
#AppStorage("totalTime") var totalTime: Double {
onChange(of: timeSelected) {
if timeSelected == "hours" {
totalTime = (Double(enteredValue) ?? 0) * multipier
} else {
totalTime = (Double(enteredValue) ?? 0)
}
}
}
#State var enteredValue = ""
#State var timeSelected = "hours"
let multipier = 2.0
let arrayLabel = ["hours", "minutes"]
var body: some View {
VStack {
Form {
Section(header: Text("Time Test")){
TextField("enter value", text: $enteredValue)
Picker("timeValue",selection: $timeSelected) {
ForEach(arrayLabel, id: \.self){
Text("\($0)")
}
}
.pickerStyle(.segmented)
}
}
}
}
}
I reworked your code so that it works like you asked, that the computation is done when the Picker is changed. However, this never triggers if the Picker isn't changed:
struct ContentView: View {
#AppStorage("totalTime") var totalTime: Double = 0.0
#State var enteredValue = ""
#State var timeSelected = "hours"
let multipier = 2.0
let arrayLabel = ["hours", "minutes"]
var body: some View {
VStack {
Form {
Section(header: Text("Time Test")){
TextField("enter value", text: $enteredValue)
Picker("timeValue",selection: $timeSelected) {
ForEach(arrayLabel, id: \.self){
Text("\($0)")
}
}
.pickerStyle(.segmented)
Text("Total time: \(totalTime, format: .number)")
}
// This can be most places within the view
.onChange(of: timeSelected) { _ in
computeTotal()
}
}
}
}
// Refactor to its own function for cleaner code.
private func computeTotal() {
if timeSelected == "hours" {
totalTime = (Double(enteredValue) ?? 0) * multipier
} else {
totalTime = (Double(enteredValue) ?? 0)
}
}
}
A better version may be:
struct TimeSelectedView: View {
#AppStorage("totalTime") var totalTime: Double = 0.0
#State var enteredValue = ""
#State var timeSelected = "hours"
// Use a computed variable that makes its computations based on enteredValue
// Every time that changes, the computed variable will update setting totalTime
var total: Double {
if timeSelected == "hours" {
totalTime = (Double(enteredValue) ?? 0) * multipier
} else {
totalTime = (Double(enteredValue) ?? 0)
}
return totalTime
}
let multipier = 2.0
let arrayLabel = ["hours", "minutes"]
var body: some View {
VStack {
Form {
Section(header: Text("Time Test")){
TextField("enter value", text: $enteredValue)
Picker("timeValue",selection: $timeSelected) {
ForEach(arrayLabel, id: \.self){
Text("\($0)")
}
}
.pickerStyle(.segmented)
Text("Total time: \(total, format: .number)")
}
}
}
}
}
This will update continuously for either a changed in enteredValue or a change in timeSelected.
According to comment in this question I made a custom SwifUI View based on a TextField. It use numeric keypad, you can't input there nothing but numbers and point, there can be only one point (dot), and you can pass a Bindable Double #State value through the View for input.
But there is a bug: when you deleting a last zero in "xxx.0" - zero still comes out. When you deleting a dot - zero becomes a part of integer, so it goes to "xxx0"
Any idea how to fix it? I tried to make value an integer when deleting last number before dot - but I can't catch the moment when there is only one last dot in a string.
here's full code:
import SwiftUI
import Combine
struct DecimalTextField: View {
public let placeHolder: String
#Binding var numericValue: Double
private class DecimalTextFieldViewModel: ObservableObject {
#Published var text = ""{
didSet{
DispatchQueue.main.async {
let substring = self.text.split(separator: Character("."), maxSplits: 2)
switch substring.count{
case 0:
if self.numericValue != 0{
self.numericValue = 0
}
case 1 :
var newValue: Double = 0
if let lastChar = substring[0].last{
if lastChar == Character("."){
newValue = Double(String(substring[0]).dropLast()) ?? 0
}else{
newValue = Double(String(substring[0])) ?? 0
}
}
self.numericValue = newValue
default:
self.numericValue = Double(String("\(String(substring[0])).\(String(substring[1]))")) ?? 0
}
}
}
}
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
#Binding private var numericValue: Double{
didSet{
DispatchQueue.main.async {
if String(self.numericValue) != self.text {
self.text = String(self.numericValue)
}
}
}
}
init(numericValue: Binding<Double>, text: String) {
self.text = text
self._numericValue = numericValue
subCancellable = $text.sink { val in
//check if the new string contains any invalid characters
if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
//clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
DispatchQueue.main.async {
self.text = String(self.text.unicodeScalars.filter {
self.validCharSet.contains($0)
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
#ObservedObject private var viewModel: DecimalTextFieldViewModel
init(placeHolder: String = "", numericValue: Binding<Double>){
self._numericValue = numericValue
self.placeHolder = placeHolder
self.viewModel = DecimalTextFieldViewModel(numericValue: self._numericValue, text: numericValue.wrappedValue == Double.zero ? "" : String(numericValue.wrappedValue))
}
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.decimalPad)
}
}
struct testView: View{
#State var numeric: Double = 0
var body: some View{
return VStack(alignment: .center){
Text("input: \(String(numeric))")
DecimalTextField(placeHolder: "123", numericValue: $numeric)
}
}
}
struct decimalTextField_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
The only way to avoid loss of the decimal point and trailing 0's while typing is to keep track of the integral and fractional digits of the string across invocations of the numeric field, which means keeping the digit precision values as part of the state of the superview. See this gist for a fully functional (Swift 5) view that uses this technique. To see what happens if you don't keep the digit precision in the superview, compare the behavior of the first and second fields in the preview below: the first will handle typing as expected, the second will remove any trailing .0 as soon as the value changes.
I'm not sure if I really do everything right, but it looks like I fix that.
here's the code:
import SwiftUI
import Combine
fileprivate func getTextOn(double: Double) -> String{
let rounded = double - Double(Int(double)) == 0
var result = ""
if double != Double.zero{
result = rounded ? String(Int(double)) : String(double)
}
return result
}
struct DecimalTextField: View {
public let placeHolder: String
#Binding var numericValue: Double
private class DecimalTextFieldViewModel: ObservableObject {
#Published var text = ""{
didSet{
DispatchQueue.main.async {
let substring = self.text.split(separator: Character("."), maxSplits: 2)
if substring.count == 0{
if self.numericValue != 0{
self.numericValue = 0
}
}else if substring.count == 1{
var newValue: Double = 0
if let lastChar = substring[0].last{
let ch = String(lastChar)
if ch == "."{
newValue = Double(String(substring[0]).dropLast()) ?? 0
}else{
newValue = Double(String(substring[0])) ?? 0
}
}
if self.numericValue != newValue{
self.numericValue = newValue
}
}else{
let newValue = Double(String("\(String(substring[0])).\(String(substring[1]))")) ?? 0
if self.numericValue != newValue{
self.numericValue = newValue
}
}
}
}
}
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
#Binding private var numericValue: Double{
didSet{
DispatchQueue.main.async {
if String(self.numericValue) != self.text {
self.text = String(self.numericValue)
}
}
}
}
init(numericValue: Binding<Double>, text: String) {
self.text = text
self._numericValue = numericValue
subCancellable = $text.sink { val in
//check if the new string contains any invalid characters
if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
//clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
DispatchQueue.main.async {
self.text = String(self.text.unicodeScalars.filter {
self.validCharSet.contains($0)
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
#ObservedObject private var viewModel: DecimalTextFieldViewModel
init(_ placeHolder: String = "", numericValue: Binding<Double>){
self._numericValue = numericValue
self.placeHolder = placeHolder
self.viewModel = DecimalTextFieldViewModel(numericValue: self._numericValue, text: getTextOn(double: numericValue.wrappedValue))
}
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.decimalPad)
}
}
struct testView: View{
#State var numeric: Double = 0
var body: some View{
return VStack(alignment: .center){
Text("input: \(String(numeric))")
DecimalTextField("123", numericValue: $numeric)
}
}
}
struct decimalTextField_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
But in debug I noticed the code in didSet executes several times. Not sure what is my mistake leading to that. Any suggestion?
In a text field, I'd like, when a user enters a number e.g. 12345, it gets formatted as 123.45. The user never needs to enter a decimal place, it just uses the 2 right most numbers as the decimal places. The field should only allow numbers too. This is for a SwiftUI project. Thanks in advance for any assistance.
Because there of a two way binding between what you enter and what is being shown in the TextField view it seems not possible to interpolate the displayed number entered. I would suggest a small hack:
create a ZStack with a TextField and a Text View superimposed.
the foreground font of the entered text in the TextField is clear or white .foregroundColor(.clear)
the keyboard is only number without decimal point: .keyboardType(.numberPad)
use .accentColor(.clear) to hide the cursor
the results are displayed in a Text View with formatting specifier: "%.2f"
It would look like
This is the code:
struct ContentView: View {
#State private var enteredNumber = ""
var enteredNumberFormatted: Double {
return (Double(enteredNumber) ?? 0) / 100
}
var body: some View {
Form {
Section {
ZStack(alignment: .leading) {
TextField("", text: $enteredNumber)
.keyboardType(.numberPad).foregroundColor(.clear)
.textFieldStyle(PlainTextFieldStyle())
.disableAutocorrection(true)
.accentColor(.clear)
Text("\(enteredNumberFormatted, specifier: "%.2f")")
}
}
}
}
}
With Swift UI the complete solution is
TextField allow numeric value only
Should accept only one comma (".")
Restrict decimal point upto x decimal place
File NumbersOnlyViewModifier
import Foundation
import SwiftUI
import Combine
struct NumbersOnlyViewModifier: ViewModifier {
#Binding var text: String
var includeDecimal: Bool
var digitAllowedAfterDecimal: Int = 1
func body(content: Content) -> some View {
content
.keyboardType(includeDecimal ? .decimalPad : .numberPad)
.onReceive(Just(text)) { newValue in
var numbers = "0123456789"
let decimalSeparator: String = Locale.current.decimalSeparator ?? "."
if includeDecimal {
numbers += decimalSeparator
}
if newValue.components(separatedBy: decimalSeparator).count-1 > 1 {
let filtered = newValue
self.text = isValid(newValue: String(filtered.dropLast()), decimalSeparator: decimalSeparator)
} else {
let filtered = newValue.filter { numbers.contains($0)}
if filtered != newValue {
self.text = isValid(newValue: filtered, decimalSeparator: decimalSeparator)
} else {
self.text = isValid(newValue: newValue, decimalSeparator: decimalSeparator)
}
}
}
}
private func isValid(newValue: String, decimalSeparator: String) -> String {
guard includeDecimal, !text.isEmpty else { return newValue }
let component = newValue.components(separatedBy: decimalSeparator)
if component.count > 1 {
guard let last = component.last else { return newValue }
if last.count > digitAllowedAfterDecimal {
let filtered = newValue
return String(filtered.dropLast())
}
}
return newValue
}
}
File View+Extenstion
extension View {
func numbersOnly(_ text: Binding<String>, includeDecimal: Bool = false) -> some View {
self.modifier(NumbersOnlyViewModifier(text: text, includeDecimal: includeDecimal))
}
}
File ViewFile
TextField("", text: $value, onEditingChanged: { isEditing in
self.isEditing = isEditing
})
.foregroundColor(Color.neutralGray900)
.numbersOnly($value, includeDecimal: true)
.font(.system(size: Constants.FontSizes.fontSize22))
.multilineTextAlignment(.center)