I'm trying to create an email input field in SwiftUI 2 that allows only certain characters to be entered. The code here is based on a piece of code from https://stackoverflow.com/a/57829567/356105.
The code itself works but since this is a reusable view component I want to provide a text property from a parent view's view model to the EmailTextField
which is updated when the text input changes ...
EmailTextField view:
import SwiftUI
import Combine
struct EmailTextField: View {
private class EmailTextFieldViewModel: ObservableObject {
@Published var text = String.Empty
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._- $!~&=#[]@")
init() {
subCancellable = $text.sink {
value in
/* Check if the new string contains any invalid characters. */
if value.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 = EmailTextFieldViewModel()
private let placeHolder: String
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.emailAddress)
.autocapitalization(.none)
.disableAutocorrection(true)
}
init(_ placeHolder: String = .Empty, text: Binding<String>) {
self.placeHolder = placeHolder
}
}
In the parent view I'm trying this:
var body: some View {
VStack {
EmailTextField("Email", text: $viewModel.email)
.onChange(of: viewModel.email, perform: onEmailInputChanged)
}
}
private func onEmailInputChanged(changedEmail: String) {
// Nothing happens here!
print("\(changedEmail)")
}
How do I need to change the EmailTextField
code to be able to bind the text
variable in its view model to the text: Binding<String>
argument in its constructor?
CodePudding user response:
Here is an alternative to your (somewhat complicated) code. I think it is simpler, provides a reusable EmailInputView
and calls to onEmailInputChanged
, and works well for me:
import SwiftUI
import Combine
class ViewModel: ObservableObject {
@Published var email = ""
}
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
VStack {
EmailInputView(placeHolder: "Email", txt: $viewModel.email)
.onChange(of: viewModel.email, perform: onEmailInputChanged)
}
}
private func onEmailInputChanged(changedEmail: String) {
print("-----> in onEmailInputChanged: \(changedEmail) ")
}
}
struct EmailInputView: View {
var placeHolder: String = ""
@Binding var txt: String
var body: some View {
TextField(placeHolder, text: $txt)
.keyboardType(.emailAddress)
.onReceive(Just(txt)) { newValue in
let validString = newValue.filter { "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._- $!~&=#[]@".contains($0) }
if validString != newValue {
self.txt = validString
}
}
}
}