I'm trying to implement some TextFields that accept any number in a desired range. This is, if the user is entering an value, I'd like it to be from min to max dynamically , for example. However, I don't know how to control this in a TextField.
struct Container {
var textInput: Double
}
struct ContentView: View {
#State private var container = Container
var body: some View {
TextField("", value: $container.textInput, format: .number)
.keyboardType(.decimalPad)
.frame(width: 200, height: 20)
.padding()
}
}
Had the exact same problem and came up with this:
Using a custom formatter – it’s not perfect but it works the way I want it to.
class BoundFormatter: Formatter {
var max: Int = 0
var min: Int = 0
func clamp(with value: Int, min: Int, max: Int) -> Int{
guard value <= max else {
return max
}
guard value >= min else {
return min
}
return value
}
func setMax(_ max: Int) {
self.max = max
}
func setMin(_ min: Int) {
self.min = min
}
override func string(for obj: Any?) -> String? {
guard let number = obj as? Int else {
return nil
}
return String(number)
}
override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
guard let number = Int(string) else {
return false
}
obj?.pointee = clamp(with: number, min: self.min, max: self.max) as AnyObject
return true
}
}
Then I use it like this:
let max: Int = 100
let min: Int = 0
var formatter: BoundFormatter {
let formatter = BoundFormatter()
formatter.setMax(self.max)
formatter.setMin(self.min)
return formatter
}
#Binding var value: Int = 0
//// VIEW BODY \\\\
TextField("Number here:", value: $value, formatter: boundFormatter)
You can even improve this version by setting min max in the formatter as bindings, so you have dynamic bounds.
class BoundFormatter: Formatter {
#Binding var max: Int
#Binding var min: Int
// you have to add initializers
init(min: Binding<Int>, max: Binding<Int>) {
self._min = min
self._max = max
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented"
}
...
}
/// usage
TextField("Number here:", value: $value, formatter: BoundFormatter(min: .constant(0), max: $max))
TextField(
.onChange(of: text, perform: {
text = String($0.prefix(1))
})
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 getting data from different sources, the variable could be a number or a string of number. How do I make sure that "(number as? NSString)" or "(number as? NSNumber)" always success? Something similar to Java optInt, which will never fail even if the number is a String. See example below:
func testNumber()
{
var number = 123
guard let a = (number as? NSNumber)?.intValue else { print("1");return; }
}
func testNumberString()
{
var number = "123"
guard let a = (number as? NSNumber)?.intValue else { print("2");return; } // this failed.
}
func testNumberToString()
{
var number = 123
guard let a = (number as? NSString)?.intValue else { print("2");return; } // this sometimes failed too depend on datasource.
}
As I understand from your question, you want an integer value at the end, no matter if the input type is string or integer.
You can achieve this by using ExpressibleByStringLiteral.
Here is the demo
extension Int: ExpressibleByStringLiteral {
public typealias StringLiteralType = String
public init(stringLiteral value: StringLiteralType) {
self = Int(value) ?? 0
}
}
This Int extension allows you to accept string value as Int and return int value. If it did not convert it will give you 0 by default.
Example
func testInt() {
let numberOne: Int = "5656"
let numberTwo: Int = 1234
print(numberOne)
print(numberTwo)
}
Or another way is to create your own ExpressibleByStringLiteral, which helps you to give default value as you want.
struct StringInt: ExpressibleByStringLiteral {
var value: Int?
init(stringLiteral value: String) {
self.value = Int("\(value)")
}
func wrapped(with defaultValue: Int) -> Int {
return self.value ?? defaultValue
}
}
Example
func testInt() {
var numberThree: StringInt = "5656"
print(numberThree.value as Any) // with nil or optional value
numberThree = "asf"
print(numberThree.wrapped(with: 15)) // with default value
/**
Output
Optional(5656)
15
*/
}
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
I'm using the DecimalField struct to place text fields in my app. However, if I use it alongside an environment object, the app freezes with a memory leak.
This is my model:
class PaymentPlan: ObservableObject {
#Published var amountBorrowed: Decimal?
}
This is my content view:
var currencyFormatter: NumberFormatter {
let nf = NumberFormatter()
nf.numberStyle = .currency
nf.isLenient = true
return nf
}
struct ContentView: View {
#EnvironmentObject var paymentPlan: PaymentPlan
static var currencyFormatter: NumberFormatter {
let nf = NumberFormatter()
nf.numberStyle = .currency
nf.isLenient = true
return nf
}
var body: some View {
DecimalField("Placeholder", value: $paymentPlan.amountBorrowed, formatter: Self.currencyFormatter)
}
}
This is the custom text field I am using (source):
import SwiftUI
import Combine
struct DecimalField : View {
let label: LocalizedStringKey
#Binding var value: Decimal?
let formatter: NumberFormatter
let onEditingChanged: (Bool) -> Void
let onCommit: () -> Void
// The text shown by the wrapped TextField. This is also the "source of
// truth" for the `value`.
#State private var textValue: String = ""
// When the view loads, `textValue` is not synced with `value`.
// This flag ensures we don't try to get a `value` out of `textValue`
// before the view is fully initialized.
#State private var hasInitialTextValue = false
init(
_ label: LocalizedStringKey,
value: Binding<Decimal?>,
formatter: NumberFormatter,
onEditingChanged: #escaping (Bool) -> Void = { _ in },
onCommit: #escaping () -> Void = {}
) {
self.label = label
_value = value
self.formatter = formatter
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
}
var body: some View {
TextField(label, text: $textValue, onEditingChanged: { isInFocus in
// When the field is in focus we replace the field's contents
// with a plain unformatted number. When not in focus, the field
// is treated as a label and shows the formatted value.
if isInFocus {
self.textValue = self.value?.description ?? ""
} else {
let f = self.formatter
let newValue = f.number(from: self.textValue)?.decimalValue
self.textValue = f.string(for: newValue) ?? ""
}
self.onEditingChanged(isInFocus)
}, onCommit: {
self.onCommit()
})
.onReceive(Just(textValue)) {
guard self.hasInitialTextValue else {
// We don't have a usable `textValue` yet -- bail out.
return
}
// This is the only place we update `value`.
self.value = self.formatter.number(from: $0)?.decimalValue
}
.onAppear(){ // Otherwise textfield is empty when view appears
self.hasInitialTextValue = true
// Any `textValue` from this point on is considered valid and
// should be synced with `value`.
if let value = self.value {
// Synchronize `textValue` with `value`; can't be done earlier
self.textValue = self.formatter.string(from: NSDecimalNumber(decimal: value)) ?? ""
}
}
.keyboardType(.decimalPad)
}
}
Any suggestions on what may not be working well? The text field works perfectly with #State.
Here is fixed part - to avoid cycling it needs to update only with really new value
Tested with Xcode 12 / iOS 14
.onReceive(Just(textValue)) {
guard self.hasInitialTextValue else {
// We don't have a usable `textValue` yet -- bail out.
return
}
// This is the only place we update `value`.
let newValue = self.formatter.number(from: $0)?.decimalValue
if newValue != self.value {
self.value = newValue
}
}
Consider the following:
struct MiniString {
private(set) var value: String
init(_ value: String) {
if value.count < 17 {
self.value = value
} else {
selfDeleteSomehow()
}
}
}
Elsewhere this could be instantiated thus:
var ms: MiniString? = MiniString("This string is too long to be accepted")
print(ms) // prints 'nil'
Context: my specific use-case is for a func declaration in a protocol that would return a Double between 0.0 and 1.0, but no higher or lower, something like:
protocol DoubleBetweenZeroAndOneProtocol {
func getResult() -> DoubleBetweenZeroAndOne
}
You could use a failable initializer:
struct MiniString {
var value: String { return value_ }
private let value_: String
init?(_ seedValue: String) {
if seedValue.count < 17 {
value_ = seedValue
} else {
return nil
}
}
}