Stop character repetition in TextField without NumberFormatter - swiftui

For example in a textfield I want to be able to repeat all numbers, but user should be able to use decimal point only once. So, the user should not be able to do: "12...4" but should only be able to do it once "12.4". Any particular way to do do it? Is it possible to do it with SwiftUI without using UIKit?
import SwiftUI
import Combine
struct TipsView: View {
#State private var amount = ""
var body: some View {
NavigationView {
Form {
Section (header: Text("Amount")) {
HStack (spacing: 1) {
Text("£")
TextField("Amount", text: $amount)
.onReceive(Just(amount)) { _ in
}
}
}
}
}
}
}

I have used a String separator to separate the input string into two different arrays. Meaning, one array would contain values before the decimal point and the other would contain values after the decimal point.
import SwiftUI
struct DecimalView: View {
#State private var amount = ""
var body: some View {
Form {
Section(header: Text("Amount")) {
HStack {
Text("£")
TextField("Enter amount", text: $amount)
.onChange(of: amount) { _ in
let filtered = amount.filter {"0123456789.".contains($0)}
if filtered.contains(".") {
let splitted = filtered.split(separator: ".")
if splitted.count >= 2 {
let preDecimal = String(splitted[0])
let afterDecimal = String(splitted[1])
amount = "\(preDecimal).\(afterDecimal)"
}
}
}
}
}
}
}
}

Related

Does SwiftUI's ForEach cache #State variables of child views beyond their existence?

So here is a little piece of code that sums up a problem I cannot figure out atm.
In the code below I add and remove entries to a dictionary keyed by an Enum.
What I would expect is that every time I add an item a new random number is being generated in the Element view and displayed.
What happens is that for every same Ident the same random number shows up - event though the ForEach loop has had a state where that Ident key was not in the dictionary any more. It appears as if ForEach does not purge the #State vars of the Element views that are not present any more, but reuses them when a new entry to the dictionary is added with the same Ident.
Is this expected behavior? What am I doing wrong?
Here is the code:
import Foundation
import SwiftUI
enum Ident:Int, Comparable, CaseIterable {
case one=1, two, three, four
static func < (lhs: Ident, rhs: Ident) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
extension Dictionary where Key == Ident,Value== String {
var asSortedArray:Array<(Ident,Value)> {
Array(self).sorted(by: { $0.key < $1.key })
}
var nextKey:Ident? {
if self.isEmpty {
return .one
}
else {
return Array(Set(Ident.allCases).subtracting(Set(self.keys))).sorted().first
}
}
}
struct ContentView: View {
#State var dictionary:[Ident:String] = [:]
var body: some View {
Form {
Section {
ForEach(dictionary.asSortedArray, id: \.0) { (ident, text) in
Element(dictionary: $dictionary, ident: ident, text: text)
}
}
Section {
Button(action: {
if let nextIdent = dictionary.nextKey {
dictionary[nextIdent] = "Something"
}
}, label: {
Text("Add one")
})
}
}
}
}
struct Element:View {
#Binding var dictionary:[Ident:String]
var ident:Ident
var text:String
#State var random:Int = Int.random(in: 0...1000)
var body: some View {
HStack {
Text(String(ident.rawValue))
Text(String(random))
Button(action: {
dictionary.removeValue(forKey: ident)
}, label: {
Text("Delete me.")
})
Spacer()
}
}
}

SwiftUI Textfields and Ints

I started today to use SwiftUI an I want to code a small calculator. Therefore I want a textfield where the user can write a number. But the usual textfields only accepts strings
what can I do ?
If you want to force the TextField keyboard to be numeric, just do this:
TextField("Input", text: $input)
.keyboardType(.decimalPad)
where the keyboardType is a UIKeyboardType.
with SwiftUI 2.0 you could use something like this:
struct ContentView: View {
#State var theInt: Int?
#State var text = ""
var body: some View {
VStack {
TextField("enter a number", text: $text)
.onChange(of: text) {
let txt = $0.filter { "-0123456789".contains($0) }
if allowed(txt) {
theInt = Int(txt)
text = txt
} else {
text = String(txt.dropLast())
}
}
}
}
func allowed(_ str: String) -> Bool {
let num = str.starts(with: "-") ? String(str.dropFirst()) : str
return CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: num))
}
}

Easiest way to make a dynamic, editable list of simple objects in SwiftUI?

I want a dynamic array of mutable strings to be presented by a mother view with a list of child views, each presenting one of the strings, editable. Also, the mother view will show a concatenation of the strings which will update whenever one of the strings are updated in the child views.
Can't use (1) ForEach(self.model.strings.indices) since set of indices may change and can't use (2) ForEach(self.model.strings) { string in since the sub views wants to edit the strings but string will be immutable.
The only way I have found to make this work is to make use of an #EnvironmentObject that is passed around along with the parameter. This is really clunky and borders on offensive.
However, I am new to swiftui and I am sure there a much better way to go about this, please let know!
Here's what I have right now:
import SwiftUI
struct SimpleModel : Identifiable { var id = UUID(); var name: String }
let simpleData: [SimpleModel] = [SimpleModel(name: "text0"), SimpleModel(name: "text1")]
final class UserData: ObservableObject { #Published var simple = simpleData }
struct SimpleRowView: View {
#EnvironmentObject private var userData: UserData
var simple: SimpleModel
var simpleIndex: Int { userData.simple.firstIndex(where: { $0.id == simple.id })! }
var body: some View {
TextField("title", text: self.$userData.simple[simpleIndex].name)
}
}
struct SimpleView: View {
#EnvironmentObject private var userData: UserData
var body: some View {
let summary_binding = Binding<String>(
get: {
var arr: String = ""
self.userData.simple.forEach { sim in arr += sim.name }
return arr;
},
set: { _ = $0 }
)
return VStack() {
TextField("summary", text: summary_binding)
ForEach(userData.simple) { tmp in
SimpleRowView(simple: tmp).environmentObject(self.userData)
}
Button(action: { self.userData.simple.append(SimpleModel(name: "new text"))}) {
Text("Add text")
}
}
}
}
Where the EnironmentObject is created and passed as SimpleView().environmentObject(UserData()) from AppDelegate.
EDIT:
For reference, should someone find this, below is the full solution as suggested by #pawello2222, using ObservedObject instead of EnvironmentObject:
import SwiftUI
class SimpleModel : ObservableObject, Identifiable {
let id = UUID(); #Published var name: String
init(name: String) { self.name = name }
}
class SimpleArrayModel : ObservableObject, Identifiable {
let id = UUID(); #Published var simpleArray: [SimpleModel]
init(simpleArray: [SimpleModel]) { self.simpleArray = simpleArray }
}
let simpleArrayData: SimpleArrayModel = SimpleArrayModel(simpleArray: [SimpleModel(name: "text0"), SimpleModel(name: "text1")])
struct SimpleRowView: View {
#ObservedObject var simple: SimpleModel
var body: some View {
TextField("title", text: $simple.name)
}
}
struct SimpleView: View {
#ObservedObject var simpleArrayModel: SimpleArrayModel
var body: some View {
let summary_binding = Binding<String>(
get: { return self.simpleArrayModel.simpleArray.reduce("") { $0 + $1.name } },
set: { _ = $0 }
)
return VStack() {
TextField("summary", text: summary_binding)
ForEach(simpleArrayModel.simpleArray) { simple in
SimpleRowView(simple: simple).onReceive(simple.objectWillChange) {_ in self.simpleArrayModel.objectWillChange.send()}
}
Button(action: { self.simpleArrayModel.simpleArray.append(SimpleModel(name: "new text"))}) {
Text("Add text")
}
}
}
}
You don't actually need an #EnvironmentObject (it will be available globally for all views in your environment).
You may want to use #ObservedObject instead (or #StateObject if using SwiftUI 2.0):
...
return VStack {
TextField("summary", text: summary_binding)
ForEach(userData.simple, id:\.id) { tmp in
SimpleRowView(userData: self.userData, simple: tmp) // <- pass userData to child views
}
Button(action: { self.userData.simple.append(SimpleModel(name: "new text")) }) {
Text("Add text")
}
}
struct SimpleRowView: View {
#ObservedObject var userData: UserData
var simple: SimpleModel
...
}
Note that if your data is not constant you should use a dynamic ForEach loop (with an explicit id parameter):
ForEach(userData.simple, id:\.id) { ...
However, the best results you can achieve when you make your SimpleModel a class and ObservableObject. Here is a better solution how do do it properly:
SwiftUI update data for parent NavigationView
Also, you can simplify your summary_binding using reduce:
let summary_binding = Binding<String>(
get: { self.userData.simple.reduce("") { $0 + $1.name } },
set: { _ = $0 }
)

Format text field to 2 decimal places without entering a decimal (SwiftUI)

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)

Conditionally Text in SwiftUI depending on Array value

I want make placeholder custom style so i try to use the method of Mojtaba Hosseini in SwiftUI. How to change the placeholder color of the TextField?
if text.isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
but in my case, I use a foreach with a Array for make a list of Textfield and Display or not the Text for simulate the custom placeholder.
ForEach(self.ListeEquip.indices, id: \.self) { item in
ForEach(self.ListeJoueurs[item].indices, id: \.self){idx in
// if self.ListeJoueurs[O][O] work
if self.ListeJoueurs[item][index].isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
}
}
How I can use dynamic conditional with a foreach ?
Now I have a another problem :
i have this code :
struct EquipView: View {
#State var ListeJoueurs = [
["saoul", "Remi"],
["Paul", "Kevin"]
]
#State var ListeEquip:[String] = [
"Rocket", "sayans"
]
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices) { item in
BulleEquip(EquipName: item, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
}
}
}
struct BulleEquip: View {
var EquipName = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
VStack{
VStack{
Text("Équipe \(EquipName+1)")
}
VStack { // Added this
ForEach(self.ListeJoueurs[EquipName].indices) { index in
ListeJoueurView(EquipNamed: self.EquipName, JoueurIndex: index, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
HStack{
Button(action: {
self.ListeJoueurs[self.EquipName].append("") //problem here
}){
Text("button")
}
}
}
}
}
}
struct ListeJoueurView: View {
var EquipNamed = 0
var JoueurIndex = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
HStack{
Text("Joueur \(JoueurIndex+1)")
}
}
}
I can run the App but I have this error in console when I click the button :
ForEach, Int, ListeJoueurView> count (3) != its initial count (2). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Can someone enlighten me?
TL;DR
You need a VStack, HStack, List, etc outside each ForEach.
Updated
For the second part of your question, you need to change your ForEach to include the id parameter:
ForEach(self.ListeJoueurs[EquipName].indices, id: \.self)
If the data is not constant and the number of elements may change, you need to include the id: \.self so SwiftUI knows where to insert the new views.
Example
Here's some example code that demonstrates a working nested ForEach. I made up a data model that matches how you were trying to call it.
struct ContentView: View {
// You can ignore these, since you have your own data model
var ListeEquip: [Int] = Array(1...3)
var ListeJoueurs: [[String]] = []
// Just some random data strings, some of which are empty
init() {
ListeJoueurs = (1...4).map { _ in (1...4).map { _ in Bool.random() ? "Text" : "" } }
}
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices, id: \.self) { item in
VStack { // Added this
ForEach(self.ListeJoueurs[item].indices, id: \.self) { index in
if self.ListeJoueurs[item][index].isEmpty { // If string is blank
Text("Placeholder")
.foregroundColor(.red)
} else { // If string is not blank
Text(self.ListeJoueurs[item][index])
}
}
}.border(Color.black)
}
}
}
}
Explanation
Here's what Apple's documentation says about ForEach:
A structure that computes views on demand from an underlying collection of of [sic] identified data.
So something like
ForEach(0..2, id: \.self) { number in
Text(number.description)
}
is really just shorthand for
Text("0")
Text("1")
Text("2")
So your ForEach is making a bunch of views, but this syntax for declaring views is only valid inside a View like VStack, HStack, List, Group, etc. The technical reason is because these views have an init that looks like
init(..., #ViewBuilder content: () -> Content)
and that #ViewBuilder does some magic that allows this unique syntax.