I have a simple TextField for telephone input, and want to format it each time it being changed.
I'm using PhoneNumberKit and it works fine, but i do not understand how to call formatting func after the value in textField have changed.
Telephone Formatting function.
import SwiftUI
import PhoneNumberKit
import Combine
func formatTelephone(telephone : String) -> String
{
do {
let phoneNumber = PartialFormatter().formatPartial(telephone)
print(phoneNumber)
return phoneNumber
}
catch {
print("Generic parser error")
}
}
It does something like this:
formatTelephone("79152140700") -> "7 (915) 214 08-00"
formatTelephone("791521") -> "7 (915) 21"
and i have a TextField like that
TextField("915 214 07 00" , text: $telephoneManager.telephone)
After each input of a digit the whole textfield label needs to be formatted by a func and show user's input in a better way.
In order to get what you want you need to use Combine (the implementation of the two methods in the view model is up to you because they strictly depend on your needs):
import SwiftUI
import Combine
class ContentViewModel: ObservableObject {
#Published var phoneNumber = ""
private var toCancel: AnyCancellable!
init() {
toCancel = $phoneNumber
.filter { !self.isFormatValid($0) }
.map { self.convert($0) }
.receive(on: RunLoop.main)
.assign(to: \.phoneNumber, on: self)
}
private func isFormatValid(_ str: String) -> Bool {
//here you must check if _str_ is already in the right format
true //just to make this example compile, but clearly this must be replaced with your code
}
private func convert(_ str: String) -> String {
//convert your string to be in the right format
"string in the correct format" //just to make this example compile, but clearly this must be replaced with your code
}
deinit {
toCancel.cancel()
}
}
struct ContentView: View {
#ObservedObject var contentViewModel = ContentViewModel()
var body: some View {
TextField("Your phone number...", text: $contentViewModel.phoneNumber)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Steps are (look at the view model init):
phoneNumber is a #Published property, so "it's backed" by a publisher you can subscribe to. So, let's subscribe to that publisher.
You must check if the string in your textfield is already in the right format. This is paramount: if you miss this step you'll end up with an infinite loop. This is done through the .filter operator. The event is forwarded down only if the .filter operator returns true (in this case it returns true if the string format is invalid).
With the .map operator you can change the string (in the wrong format) and return the string in the right format.
.receive lets you forward the event on the thread you are interested in. In this case the main thread (since you want to update a textfield).
.assign just assigns the new value to the textfield.
Related
I am working on a registration screen which has a ViewModel attached to it.
The ViewModel is just a class which extends my registration screen class, where I place all of my business logic.
extension RegistrationScreen {
#MainActor class ViewModel : ObservableObject {
//business logic goes here
}
}
The ViewModel has #Published variables to represent two states for every text field in the screen: text and validationText.
#Published var first: String = ""
#Published var firstValidationMessage: String = ""
within that ViewModel I have a call to a helper function from another class which checks to see if the field's text is empty and if so it can set the fields validation text to an error, that looks like this:
class FieldValidation: Identifiable {
func isFieldEmptyAndSetError(fieldText:String, fieldValidationText:Binding<String>) -> Bool {
if(fieldText.isEmpty){
fieldValidationText.wrappedValue = "Required"
return true
}else{
fieldValidationText.wrappedValue = ""
return false
}
}
}
to call that function from my viewModel I do the following:
FieldValidation().isFieldEmptyAndSetError(fieldText:first,fieldValidationText: firstValidationMessage)
This is throwing runtime errors which are hard to decrypt for me since I am new to Xcode and iOS in general.
My question is, how can I get this passing by reference to work? alternatively if the way I am doing is not possible please explain what is going on.
It's kind of weird having a function that receives a #Published as a parameter, if you want to handle all the validations in the viewModel, this seems as an easy solution for Combine. I would suggest you to create an extension to validate if the string is empty, since .isEmpty does not trim the text.
The class
class FieldValidation: Identifiable {
static func isFieldEmptyAndSetError(fieldText:String) -> String {
return fieldText.isEmpty ? "Required" : ""
}
}
The view Model
import Foundation
import Combine
class viewModel: ObservableObject {
#Published var text1 = ""
#Published var text2 = ""
#Published var text3 = ""
#Published var validationText1 = ""
#Published var validationText2 = ""
#Published var validationText3 = ""
init() {
self.$text1.map{newText in FieldValidation.isFieldEmptyAndSetError(fieldText: newText)}.assign(to: &$validationText1)
self.$text2.map{newText in FieldValidation.isFieldEmptyAndSetError(fieldText: newText)}.assign(to: &$validationText2)
}
func validateText(value: String) {
self.validationText3 = FieldValidation.isFieldEmptyAndSetError(fieldText: value)
}
}
The view would look like this:
import SwiftUI
struct ViewDate: View {
#StateObject var myviewModel = viewModel()
var body: some View {
VStack{
Text("Fill the next fields:")
TextField("", text: $myviewModel.text1)
.border(Color.red)
Text(myviewModel.validationText1)
TextField("", text: $myviewModel.text2)
.border(Color.blue)
Text(myviewModel.validationText2)
TextField("", text: $myviewModel.text3)
.border(Color.green)
.onChange(of: myviewModel.text3) { newValue in
if(newValue.isEmpty){
self.myviewModel.validateText(value: newValue)
}
}
Text(myviewModel.validationText3)
}.padding()
}
}
I've been trying to find a good way to connect a TextField with a viewmodel, so that the inputs to the TextField don't immediately update the view.
Here is a working example:
struct TextFieldSamples: View {
#StateObject var vm = TFViewModel()
var body: some View {
print(Self._printChanges())
return VStack {
TextField("placeholder",
text: Binding(
get: { vm.outputText },
set: { vm.subject.value = $0 }
))
Text(vm.outputText) // this will only update when the textfield holds >5 characters
}
}
}
class TFViewModel: ObservableObject {
#Published var outputText: String = ""
let subject = CurrentValueSubject<String, Never>("")
var subscriptions = Set<AnyCancellable>()
init() {
subject
.filter({ $0.count > 5 })
.sink { self.outputText = $0 }
.store(in: &subscriptions)
}
}
However, I came across the propertyWrapped CurrentValueSubject below on the swiftbysundell.com blog
#propertyWrapper
struct Input<Value> {
var wrappedValue: Value {
get { subject.value }
set { subject.send(newValue) }
}
var projectedValue: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue: Value) {
subject = CurrentValueSubject(wrappedValue)
}
}
Trying to use it, will result in the error Binding<String> action tried to update multiple times per frame.
(Not only that, the input behavior of the TextField gets messed up. This shows especially with Japanese input where the character transformations don't are not working most of the time.)
Here's a example showing this behavior:
struct TextFieldSamples: View {
#StateObject var vm = TFViewModel()
var body: some View {
print(Self._printChanges())
return VStack {
TextField("placeholder", text: $vm.inputText)
Text(vm.outputText)
}
}
}
class TFViewModel: ObservableObject {
#Input var inputText: String = ""
#Published var outputText: String = ""
var subscriptions = Set<AnyCancellable>()
init() {
$inputText
.filter({ $0.count > 5 })
.sink { self.outputText = $0 }
.store(in: &subscriptions)
}
}
I don't quite understand why the PropertyWrapper version is not working as expected.
The issue appears to a combination of the binding to the inputText field of vm and the binding to the vm itself as a #StateObject.
When text is entered, It sends the value to the inputText binding. This allows the system to flag that the TextField needs to be re-evaluated.
But inputText is a struct property of the vm object. Because vm is a #StateObject when inputText changes, the vm object broadcasts that the TextFieldSamples view needs to be re-evaluated. As part of that it also tries to set the value of textInput (to the value it already holds) which causes the "binding fired multiple times in the same frame" message.
You can eliminate the extra time the Binding fires by making vm a #State property instead of #StateObject. Now the change to inputText will fire once, and vm will not broadcast the object change message that tries to fire it the second time.
However, when you do that the system won't know TextFieldSamples needs to be reevaluated so it will never see the change to outputText.
I've created a property wrapper that I want to insert some logic into and the "set" value is doing the right thing, but the textfield isn't updating with all uppercase text. Shouldn't the text field be showing all uppercase text or am I misunderstanding how this is working?
Also this is a contrived example, my end goal is to insert a lot more logic into a property wrapper, I'm just using the uppercase example to get it working. I've searched all over the internet and haven't found a working solution.
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var vm = FormDataViewModel()
var body: some View {
Form {
TextField("Name", text: $vm.name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class FormDataViewModel: ObservableObject {
#Capitalized var name: String = ""
}
#propertyWrapper
public class Capitalized {
#Published var value: String
public var wrappedValue: String {
get { value }
set { value = newValue.uppercased() } //Printing this shows all caps
}
public var projectedValue: AnyPublisher<String, Never> {
return $value
.eraseToAnyPublisher()
}
public init(wrappedValue: String) {
value = wrappedValue
}
}
SwiftUI watches #Published properties in #StateObject or #ObservedObject and triggers UI update on changes of them.
But it does not go deep inside the ObservableObject. Your FormDataViewModel does not have any #Published properties.
One thing you can do is that simulating what #Published will do on value changes.
class FormDataViewModel: ObservableObject {
#Capitalized var name: String = ""
private var nameObserver: AnyCancellable?
init() {
nameObserver = _name.$value.sink {_ in
self.objectWillChange.send()
}
}
}
Please try.
This can be done with standard #Published that seems simpler and more reliable.
Here is a solution. Tested with Xcode 12 / iOS 14.
class FormDataViewModel: ObservableObject {
#Published var name: String = "" {
didSet {
let capitalized = name.uppercased()
if name != capitalized {
name = capitalized
objectWillChange.send()
}
}
}
}
This question relates to this one: How to observe a TextField value with SwiftUI and Combine?
But what I am asking is a bit more general.
Here is my code:
struct MyPropertyStruct {
var text: String
}
class TestModel : ObservableObject {
#Published var myproperty = MyPropertyStruct(text: "initialText")
func saveTextToFile(text: String) {
print("this function saves text to file")
}
}
struct ContentView: View {
#ObservedObject var testModel = TestModel()
var body: some View {
TextField("", text: $testModel.myproperty.text)
}
}
Scenario: As the user types into the textfield, the saveTextToFile function should be called. Since this is saving to a file, it should be slowed-down/throttled.
So My question is:
Where is the proper place to put the combine operations in the code below.
What Combine code do I put to accomplish: (A) The string must not contain spaces. (B) The string must be 5 characters long. (C) The String must be debounced/slown down
I wanted to use the response here to be a general pattern of: How should we handle combine stuff in a SwiftUI app (not UIKit app).
You should do what you want in your ViewModel. Your view model is the TestModel class (which I suggest you rename it in TestViewModel). It's where you are supposed to put the logic between the model and the view. The ViewModel should prepare the model to be ready for the visualization. And that is the right place to put your combine logic (if it's related to the view, of course).
Now we can use your specific example to actually make an example. To be honest there are a couple of slight different solutions depending on what you really want to achieve. But for now I'll try to be as generic as possible and then you can tell me if the solution is fine or it needs some refinements:
struct MyPropertyStruct {
var text: String
}
class TestViewModel : ObservableObject {
#Published var myproperty = MyPropertyStruct(text: "initialText")
private var canc: AnyCancellable!
init() {
canc = $myproperty.debounce(for: 0.5, scheduler: DispatchQueue.main).sink { [unowned self] newText in
let strToSave = self.cleanText(text: newText.text)
if strToSave != newText.text {
//a cleaning has actually happened, so we must change our text to reflect the cleaning
self.myproperty.text = strToSave
}
self.saveTextToFile(text: strToSave)
}
}
deinit {
canc.cancel()
}
private func cleanText(text: String) -> String {
//remove all the spaces
let resultStr = String(text.unicodeScalars.filter {
$0 != " "
})
//take up to 5 characters
return String(resultStr.prefix(5))
}
private func saveTextToFile(text: String) {
print("text saved")
}
}
struct ContentView: View {
#ObservedObject var testModel = TestViewModel()
var body: some View {
TextField("", text: $testModel.myproperty.text)
}
}
You should attach your own subscriber to the TextField publisher and use the debounce publisher to delay the cleaning of the string and the calling to the saving method. According to the documentation:
debounce(for:scheduler:options:)
Use this operator when you want to wait for a pause in the delivery of
events from the upstream publisher. For example, call debounce on the
publisher from a text field to only receive elements when the user
pauses or stops typing. When they start typing again, the debounce
holds event delivery until the next pause.
When the user stops typing the debounce publisher waits for the specified time (in my example here above 0.5 secs) and then it calls its subscriber with the new value.
The solution above delays both the saving of the string and the TextField update. This means that users will see the original string (the one with spaces and maybe longer than 5 characters) for a while, before the update happens. And that's why, at the beginning of this answer, I said that there were a couple of different solutions depending on the needs. If, indeed, we want to delay just the saving of the string, but we want the users to be forbidden to input space characters or string longer that 5 characters, we can use two subscribers (I'll post just the code that changes, i.e. the TestViewModel class):
class TestViewModel : ObservableObject {
#Published var myproperty = MyPropertyStruct(text: "initialText")
private var saveCanc: AnyCancellable!
private var updateCanc: AnyCancellable!
init() {
saveCanc = $myproperty.debounce(for: 0.5, scheduler: DispatchQueue.main)
.map { [unowned self] in self.cleanText(text: $0.text) }
.sink { [unowned self] newText in
self.saveTextToFile(text: self.cleanText(text: newText))
}
updateCanc = $myproperty.sink { [unowned self] newText in
let strToSave = self.cleanText(text: newText.text)
if strToSave != newText.text {
//a cleaning has actually happened, so we must change our text to reflect the cleaning
DispatchQueue.main.async {
self.myproperty.text = strToSave
}
}
}
}
deinit {
saveCanc.cancel()
updateCanc.cancel()
}
private func cleanText(text: String) -> String {
//remove all the spaces
let resultStr = String(text.unicodeScalars.filter {
$0 != " "
})
//take up to 5 characters
return String(resultStr.prefix(5))
}
private func saveTextToFile(text: String) {
print("text saved: \(text)")
}
}
How to create a swiftui textfield that allows the user to only input numbers and a single dot?
In other words, it checks digit by digit as the user inputs, if the input is a number or a dot and the textfield doesn't have another dot the digit is accepted, otherwise the digit entry is ignored.
Using a stepper isn't an option.
SwiftUI doesn't let you specify a set of allowed characters for a TextField. Actually, it's not something related to the UI itself, but to how you manage the model behind. In this case the model is the text behind the TextField. So, you need to change your view model.
If you use the $ sign on a #Published property you can get access to the Publisher behind the #Published property itself. Then you can attach your own subscriber to the publisher and perform any check you want. In this case I used the sink function to attach a closure based subscriber to the publisher:
/// Attaches a subscriber with closure-based behavior.
///
/// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber.
/// - parameter receiveValue: The closure to execute on receipt of a value.
/// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream.
public func sink(receiveValue: #escaping ((Self.Output) -> Void)) -> AnyCancellable
The implementation:
import SwiftUI
import Combine
class ViewModel: ObservableObject {
#Published var text = ""
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
init() {
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()
}
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField("Type something...", text: $viewModel.text)
}
}
Important to note that:
$text ($ sign on a #Published property) gives us an object of type Published<String>.Publisher i.e. a publisher
$viewModel.text ($ sign on an #ObservableObject) gives us an object of type Binding<String>
That are two completely different things.
EDIT: If you want you can even create you own custom TextField with this behaviour. Let's say you want to create a DecimalTextField view:
import SwiftUI
import Combine
struct DecimalTextField: View {
private class DecimalTextFieldViewModel: ObservableObject {
#Published var text = ""
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
init() {
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()
var body: some View {
TextField("Type something...", text: $viewModel.text)
}
}
struct ContentView: View {
var body: some View {
DecimalTextField()
}
}
This way you can use your custom text field just writing:
DecimalTextField()
and you can use it wherever you want.
This is a simple solution for TextField validation: (updated)
struct ContentView: View {
#State private var text = ""
func validate() -> Binding<String> {
let acceptableNumbers: String = "0987654321."
return Binding<String>(
get: {
return self.text
}) {
if CharacterSet(charactersIn: acceptableNumbers).isSuperset(of: CharacterSet(charactersIn: $0)) {
print("Valid String")
self.text = $0
} else {
print("Invalid String")
self.text = $0
self.text = ""
}
}
}
var body: some View {
VStack {
Spacer()
TextField("Text", text: validate())
.padding(24)
Spacer()
}
}
}
I think using an async dispatch is the wrong approach and may cause other issues. Here's an implementation that achieves the same thing with a Double-backed property and manually iterates over the characters each time you type in the bound view.
final class ObservableNumber: ObservableObject {
let precision: Int
#Published
var value: String {
didSet {
var decimalHit = false
var remainingPrecision = precision
let filtered = value.reduce(into: "") { result, character in
// If the character is a number that by adding wouldn't exceed the precision and precision is set then add the character.
if character.isNumber, remainingPrecision > 0 || precision <= 0 {
result.append(character)
// If a decimal has been hit then decrement the remaining precision to fulfill
if decimalHit {
remainingPrecision -= 1
}
// If the character is a decimal, one hasn't been added already, and precision greater than zero then add the decimal.
} else if character == ".", !result.contains("."), precision > 0 {
result.append(character)
decimalHit = true
}
}
// Only update value if after processing it is a different value.
// It will hit an infinite loop without this check since the published event occurs as a `willSet`.
if value != filtered {
value = filtered
}
}
}
var doubleValue: AnyPublisher<Double, Never> {
return $value
.map { Double($0) ?? 0 }
.eraseToAnyPublisher()
}
init(precision: Int, value: Double) {
self.precision = precision
self.value = String(format: "%.\(precision)f", value)
}
}
This solution also ensures you only have a single decimal instead of allowing multiple instances of ".".
Note the extra computed property to put it "back" into a Double. This allows you to continue to react to the number as a number instead of a String and have to cast/convert everywhere. You could very easily add as many computed properties as you want to react to it as Int or whatever numeric type as long as you convert it in the way you would expect.
One more note you could also make it generic ObservableNumber<N: Numeric> and handle different inputs but using Double and keeping the generics out of it will simplify other things down the road. Change according to your needs.
Easy solution is to set .numberPad keyboardType:
TextField(
"0.0",
text: $fromValue
)
.keyboardType(UIKeyboardType.numberPad)