Make iOS SwiftUI TextField reject bad numeric input - ios

My iOS SwiftUI TextField allows the user to input an Int. If the user types bad input characters such as "abc" instead of "123", I would like to display the bad characters in an error message in my Text (where I wrote textField.text), and I would like to keep the TextField's keyboard on the screen until the user provides correct input. How to make this happen? Thank you in advance. Xcode 11.6 on macOS Catalina 10.15.6.
struct ContentView: View {
#State private var value: Int? = nil;
#State private var text: String = "";
var body: some View {
VStack {
TextField(
"Enter an integer:",
value: $value,
formatter: NumberFormatter(),
onCommit: {
guard let value: Int = self.value else {
self.text = "Bad input \"\(textField.text)\".";
//Do not dismiss the TextField's keyboard.
return;
}
//Dismiss the TextField's keyboard.
self.text = "The product is \(2 * value).";
}
)
.keyboardType(.numbersAndPunctuation)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text(text)
}
.padding([.leading, .trailing], 16)
}
}

Below code gives you an idea for validating your text inside the textfield while user is typing.
struct ContentView: View {
#State private var textNumber : String = ""
#State private var isNumberValid : Bool = true
var body: some View {
VStack {
TextField("Enter an integer:", text: numberValidator())
.keyboardType(.numbersAndPunctuation)
.textFieldStyle(RoundedBorderTextFieldStyle())
if !self.isNumberValid {
Text("Bad input \"\(textNumber)\".")
.font(.callout)
.foregroundColor(Color.red)
}
}
.padding([.leading, .trailing], 16)
}
private func numberValidator() -> Binding<String> {
return Binding<String>(
get: {
return self.textNumber
}) {
if CharacterSet(charactersIn: "1234567890").isSuperset(of: CharacterSet(charactersIn: $0)) {
self.textNumber = $0
self.isNumberValid = true
} else {
self.textNumber = $0
//self.textNumber = ""
self.isNumberValid = false
}
}
}
}

The following code snippet gives you an idea to validate your text inside the text field as the user is typing using Get, Set value
let checkValue = Binding<String>(
get: {
self.value
},
set: {
self.value = $0
}
)
I hope it can be of help to you
struct ContentView: View {
#State private var value: String = ""
#State private var text: String = ""
var body: some View {
let checkValue = Binding<String>(
get: {
self.value
},
set: {
self.value = $0
}
)
return VStack {
TextField("Enter an integer:",text: checkValue)
.keyboardType(.numbersAndPunctuation)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("Bad interger: \(Int(self.value) != nil ? "" : self.value)").foregroundColor(Color.red)
}
.padding([.leading, .trailing], 16)
}
}

Related

Creating an iOS passcode view with SwiftUI, how to hide a TextView?

I am trying to imitate a lock screen of iOS in my own way with some basic code. However I do not understand how to properly hide an input textview. Now I am using an opacity modifier, but it does not seem to be the right solution. Could you please recommend me better options?
import SwiftUI
public struct PasscodeView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var viewModel: ContentView.ViewModel
private let maxDigits: Int = 4
private let userPasscode = "1234"
#State var enteredPasscode: String = ""
#FocusState var keyboardFocused: Bool
#State private var showAlert = false
#State private var alertMessage = "Passcode is wrong, try again!"
public var body: some View {
VStack {
HStack {
ForEach(0 ..< maxDigits) {
($0 + 1) > enteredPasscode.count ?
Image(systemName: "circle") :
Image(systemName: "circle.fill")
}
}
.alert("Wrong Passcode", isPresented: $showAlert) {
Button("OK", role: .cancel) { }
}
TextField("Enter your passcode", text: $enteredPasscode)
.opacity(0)
.keyboardType(.decimalPad)
.focused($keyboardFocused)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
keyboardFocused = true
}
}
}
.padding()
.onChange(of: enteredPasscode) { _ in
guard enteredPasscode.count == maxDigits else { return }
passcodeValidation()
}
}
func passcodeValidation() {
if enteredPasscode == userPasscode {
viewModel.isUnlocked = true
dismiss()
} else {
enteredPasscode = ""
showAlert = true
}
}
}

How to append more than one Number into Binding var

I'm new to swift and I'm trying to develop a Velocity Calculator.
Here is my Code:
struct VelocityCalc: View {
#State var velocityNumbers1 : [String] = []
var body: some View {
VStack {
VStack {
Text("Headline")
TextField("e.g., 1, 3, 5, 8,...", text: $velocityNumbers1)
Button {
print("Button works")
} label: {
Text("Tap me")
}
}
}
}
What I want to develop is that the User can type in for example: 12, 14, 12, 10, ...
This Numbers needs to be sorted and so on.
Maybe someone can help me with this Issue or give me some advisory for that.
Big thanks for your help :)
I have seen answers, however what I have found out that when you enter the numbers the way you showed us on your question ex: 2, 1, 5, 9 with Space or WhiteSpace it won't work as expected so here it is a solution to overcome this problem:
#State var velocityNumbers = ""
func reorderTheArray(velocity: String) -> [String] {
let orderVelocity = velocity.components(separatedBy: ",").compactMap{
Int($0.trimmingCharacters(in: .whitespaces))
}
return orderVelocity.sorted().compactMap {
String($0)
}
}
var body: some View {
VStack {
Text("Headline")
TextField("example", text: self.$velocityNumbers)
Button(action: {
self.velocityNumbers = reorderTheArray(velocity: self.velocityNumbers).joined(separator: ",")
print(self.velocityNumbers)
}) {
Text("Reorder")
}
}
}
Now when you click the Reorder button, everything will be reordered on your textfield directly.
Try something like this,
Get the numbers as a string
Split them using separator(')
Convert them into Int and sort
struct ContentView: View {
#State var velocityNumber : String = ""
var body: some View {
VStack {
VStack {
Text("Headline")
TextField("e.g., 1, 3, 5, 8,...", text: $velocityNumber)
Button {
let allNumbers = velocityNumber.split(separator: ",").compactMap {
Int($0)
}
print(allNumbers.sorted())
} label: {
Text("Tap me")
}
}
}
}
}
I would see it like this:
First i would take all numbers as a string, then split the string using the separator ",", then convert all strings to an int array and sort
struct VelocityCalc: View {
#State var velocityNumbers1 : String
var body: some View {
VStack {
VStack {
Text("Headline")
TextField("e.g., 1, 3, 5, 8,...", text: $velocityNumbers1)
Button {
let velocityNumbersArray = velocityNumbers1
.components(separatedBy: ",")
.map { Int($0)! }.sorted()
print(velocityNumbersArray)
} label: {
Text("Tap me")
}
}
}
}
}
I think it makes more sense to enter one value at a time and use a separate field to display the entered values
#State var velocityNumbers : [Int] = []
#State var velocity: String = ""
var body: some View {
VStack {
VStack {
Text("Headline")
TextField("velocity", text: $velocity)
Button {
if let value = Int(velocity) {
velocityNumbers.append(value)
velocityNumbers.sort()
}
velocity = ""
} label: {
Text("Add")
}
.keyboardShortcut(.defaultAction)
Divider()
Text(velocityNumbers.map(String.init).joined(separator: ", "))
}
}
}

Checking a toggled Button for a Quiz Game in SwiftUI

First, sorry for my bad English! I'm absolutely new to SwiftUI and I tried to create a Quiz App with multiple Choices and multiple Answers. I created a Button with a ForEach to Display the possible answers. Now I want to select the correct Answer and tap the check Button to validate the chosen Answer. There can be more then 1 correct Answer.
I tried this function but its only return, if there are one or two
//MARK:- Funktionen
func checkAnswer() {
if validateAnswer == quiz.correctAnswer {
print("Richtig")
} else {
print("Falsch")
}
}
I have no idea how to validate the chosen Answers with the correct answers. Can anyone help me?
Here is my Code:
QuizModel
struct Quiz: Identifiable {
var id: String = UUID().uuidString
var question: String
var howManyAnswers: String
var options: [PossibleAnswer]
var correctAnswer: [String]
var explain: String
}
extension Quiz: Equatable {}
struct PossibleAnswer : Identifiable, Equatable {
let id = UUID()
let text : String
}
ContentView
struct ContentView: View {
var quiz: Quiz
#State var isChecked:Bool = false
#State private var showAlert: Bool = false
#State var validateAnswer: [String] = ["Antwort 3", "Antwort 4"]
//MARK:- Answers
VStack {
ForEach(quiz.options) { answerOption in
QuizButtonView(isChecked: isChecked, title: answerOption.text)
.foregroundColor(.black)
.padding(2.0)
}
Spacer()
Divider()
HStack {
//MARK:- Button Überprüfen & Zurück
Button(action: {
print("Ich gehe zurück")
}, label: {
Text("Zurück")
})
Button(action: {
checkAnswer()
print("Ich überprüfe...")
self.showAlert.toggle()
}, label: {
Text("Überprüfen")
})
.padding(.leading, 200)
And my CheckButtonView
struct QuizButtonView: View {
#State var isChecked:Bool = false
var title:String
func toggle(){
isChecked.toggle()
if self.isChecked == true {
print("Antwort wurde ausgewählt")
} else if self.isChecked == false {
print("Antwort wurde wieder abgewählt")
}
}
var body: some View {
VStack {
Button(action: toggle) {
HStack{
Text(title)
.font(.system(size: 16))
.foregroundColor(.black)
.lineLimit(3)
Spacer()
Image(systemName: isChecked ? "checkmark.square.fill": "square")
}
}
Thank you!
You just need to create a state variable of an array of booleans:
struct ContentView: View {
private let quiz: Quiz
#State private var userSelections: [Bool]
init(quiz: Quiz) {
self.quiz = quiz
_userSelections = State(initialValue: Array(repeating: false, count: quiz.options.count))
}
var body: some View {
VStack {
ForEach(0..<quiz.options.count) { index in
QuizButtonView(isChecked: userSelections[index], title: quiz.options[index].text)
.foregroundColor(.black)
.padding(2.0)
}
}
}
func checkAnswer() {
let userSelectionTexts = Set(userSelections.enumerated().map({ quiz.options[$0.offset].text }))
let correctAnswers = Set(quiz.correctAnswer)
let isAllSelectionsTrue = userSelectionTexts == correctAnswers
let isAllSelectionsFalse = userSelectionTexts.intersection(correctAnswers).isEmpty
let isAnySelectionsTrue = !isAllSelectionsFalse
}
}

How to set textfield character limit SwiftUI?

I'm using SwiftUi version 2 for my application development. I'm facing issue with textfield available in SwiftUI. I don't want to use UITextField anymore. I want to limit the number of Characters in TextField. I searched a lot and i find some answer related to this but those answer doesn't work for SwiftUI version 2.
class textBindingManager: ObservableObject{
let characterLimit: Int
#Published var phoneNumber = "" {
didSet {
if phoneNumber.count > characterLimit && oldValue.count <= characterLimit {
phoneNumber = oldValue
}
}
}
init(limit: Int = 10) {
characterLimit = limit
}
}
struct ContentView: View {
#ObservedObject var textBindingManager = TextBindingManager(limit: 5)
var body: some View {
TextField("Placeholder", text: $textBindingManager.phoneNumber)
}
}
No need to use didSet on your published property. You can add a modifier to TextField and limit the string value to its prefix limited to the character limit:
import SwiftUI
struct ContentView: View {
#ObservedObject var textBindingManager = TextBindingManager(limit: 5)
var body: some View {
TextField("Placeholder", text: $textBindingManager.phoneNumber)
.padding()
.onChange(of: textBindingManager.phoneNumber, perform: editingChanged)
}
func editingChanged(_ value: String) {
textBindingManager.phoneNumber = String(value.prefix(textBindingManager.characterLimit))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class TextBindingManager: ObservableObject {
let characterLimit: Int
#Published var phoneNumber = ""
init(limit: Int = 10){
characterLimit = limit
}
}
The following should be the simpliest. It limits the number of characters to 10.
struct ContentView: View {
#State var searchKey: String = ""
var body: some View {
TextField("Enter text", text: $searchKey)
.onChange(of: searchKey) { newValue in
if newValue.count > 10 {
self.searchKey = String(newValue.prefix(10))
}
}
}
}
This solution wraps everything up in a new Component. You could adapt this to perform other parsing / pattern checking quite easily.
struct ContentView : View {
#State private var myTextValue: String = ""
var body: some View {
LimitedTextField(value: $myTextValue, charLimit: 2)
}
}
struct LimitedTextField : View {
#State private var enteredString: String = ""
#Binding var underlyingString: String
let charLimit : Int
init(value: Binding<String>, charLimit: Int) {
_underlyingString = value
self.charLimit = charLimit
}
var body: some View {
HStack {
TextField("", text: $enteredString, onCommit: updateUnderlyingValue)
.onAppear(perform: { updateEnteredString(newUnderlyingString: underlyingString) })
.onChange(of: enteredString, perform: updateUndelyingString)
.onChange(of: underlyingString, perform: updateEnteredString)
}
}
func updateEnteredString(newUnderlyingString: String) {
enteredString = String(newUnderlyingString.prefix(charLimit))
}
func updateUndelyingString(newEnteredString: String) {
if newEnteredString.count > charLimit {
self.enteredString = String(newEnteredString.prefix(charLimit))
underlyingString = self.enteredString
}
}
func updateUnderlyingValue() {
underlyingString = enteredString
}
}

Focus on the next TextField/SecureField in SwiftUI

I've built a login screen in SwiftUI. I want to focus on the password SecureField when the user is finished entering their email. How can I do this?
struct LoginView: View {
#State var username: String = ""
#State var password: String = ""
var body: some View {
ScrollView {
VStack {
TextField("Email", text: $username)
.padding()
.frame(width: 300)
.background(Color(UIColor.systemGray5))
.cornerRadius(5.0)
.padding(.bottom, 20)
.keyboardType(.emailAddress)
SecureField("Password", text: $password)
.padding()
.frame(width: 300)
.background(Color(UIColor.systemGray5))
.cornerRadius(5.0)
.padding(.bottom, 20)
Button(action: {
}, label: {
Text("Login")
.padding()
.frame(width: 300)
.background((username.isEmpty || password.isEmpty) ? Color.gray : Color(UIColor.cricHQOrangeColor()))
.foregroundColor(.white)
.cornerRadius(5.0)
.padding(.bottom, 20)
}).disabled(username.isEmpty || password.isEmpty)
iOS 15+
In iOS 15 we can now use #FocusState to control which field should be focused.
Here is an example how to add buttons above the keyboard to focus the previous/next field:
struct ContentView: View {
#State private var email: String = ""
#State private var username: String = ""
#State private var password: String = ""
#FocusState private var focusedField: Field?
var body: some View {
NavigationView {
VStack {
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
TextField("Username", text: $username)
.focused($focusedField, equals: .username)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
}
.toolbar {
ToolbarItem(placement: .keyboard) {
Button(action: focusPreviousField) {
Image(systemName: "chevron.up")
}
.disabled(!canFocusPreviousField()) // remove this to loop through fields
}
ToolbarItem(placement: .keyboard) {
Button(action: focusNextField) {
Image(systemName: "chevron.down")
}
.disabled(!canFocusNextField()) // remove this to loop through fields
}
}
}
}
}
extension ContentView {
private enum Field: Int, CaseIterable {
case email, username, password
}
private func focusPreviousField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue - 1) ?? .password
}
}
private func focusNextField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue + 1) ?? .email
}
}
private func canFocusPreviousField() -> Bool {
guard let currentFocusedField = focusedField else {
return false
}
return currentFocusedField.rawValue > 0
}
private func canFocusNextField() -> Bool {
guard let currentFocusedField = focusedField else {
return false
}
return currentFocusedField.rawValue < Field.allCases.count - 1
}
}
When using UIKit, one would accomplish this by setting up the responder chain. This isn't available in SwiftUI, so until there is a more sophisticated focus and responder system, you can make use of the onEditingChanged changed of TextField
You will then need to manage the state of each field based on stored State variables. It may end up being more work than you want to do.
Fortunately, you can fall back to UIKit in SwiftUI by using UIViewRepresentable.
Here is some code that manages the focus of text fields using the UIKit responder system:
import SwiftUI
struct KeyboardTypeView: View {
#State var firstName = ""
#State var lastName = ""
#State var focused: [Bool] = [true, false]
var body: some View {
Form {
Section(header: Text("Your Info")) {
TextFieldTyped(keyboardType: .default, returnVal: .next, tag: 0, text: self.$firstName, isfocusAble: self.$focused)
TextFieldTyped(keyboardType: .default, returnVal: .done, tag: 1, text: self.$lastName, isfocusAble: self.$focused)
Text("Full Name :" + self.firstName + " " + self.lastName)
}
}
}
}
struct TextFieldTyped: UIViewRepresentable {
let keyboardType: UIKeyboardType
let returnVal: UIReturnKeyType
let tag: Int
#Binding var text: String
#Binding var isfocusAble: [Bool]
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.keyboardType = self.keyboardType
textField.returnKeyType = self.returnVal
textField.tag = self.tag
textField.delegate = context.coordinator
textField.autocorrectionType = .no
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if isfocusAble[tag] {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldTyped
init(_ textField: TextFieldTyped) {
self.parent = textField
}
func updatefocus(textfield: UITextField) {
textfield.becomeFirstResponder()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if parent.tag == 0 {
parent.isfocusAble = [false, true]
parent.text = textField.text ?? ""
} else if parent.tag == 1 {
parent.isfocusAble = [false, false]
parent.text = textField.text ?? ""
}
return true
}
}
}
You can refer to this question to get more information about this particular approach.
Hope this helps!
I've improved on the answer from Gene Z. Ragan and Razib Mollick. Fixes a crash, this allows for any amount of textfields, supports passwords and made it into its own class.
struct UITextFieldView: UIViewRepresentable {
let contentType: UITextContentType
let returnVal: UIReturnKeyType
let placeholder: String
let tag: Int
#Binding var text: String
#Binding var isfocusAble: [Bool]
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.textContentType = contentType
textField.returnKeyType = returnVal
textField.tag = tag
textField.delegate = context.coordinator
textField.placeholder = placeholder
textField.clearButtonMode = UITextField.ViewMode.whileEditing
if textField.textContentType == .password || textField.textContentType == .newPassword {
textField.isSecureTextEntry = true
}
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if uiView.window != nil {
if isfocusAble[tag] {
if !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
} else {
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: UITextFieldView
init(_ textField: UITextFieldView) {
self.parent = textField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
// Without async this will modify the state during view update.
DispatchQueue.main.async {
self.parent.text = textField.text ?? ""
}
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
setFocus(tag: parent.tag)
return true
}
func setFocus(tag: Int) {
let reset = tag >= parent.isfocusAble.count || tag < 0
if reset || !parent.isfocusAble[tag] {
var newFocus = [Bool](repeatElement(false, count: parent.isfocusAble.count))
if !reset {
newFocus[tag] = true
}
// Without async this will modify the state during view update.
DispatchQueue.main.async {
self.parent.isfocusAble = newFocus
}
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
setFocus(tag: parent.tag + 1)
return true
}
}
}
struct UITextFieldView_Previews: PreviewProvider {
static var previews: some View {
UITextFieldView(contentType: .emailAddress,
returnVal: .next,
placeholder: "Email",
tag: 0,
text: .constant(""),
isfocusAble: .constant([false]))
}
}
Here you go - Native SwiftUI solution. Thanks Gene Z. Ragan for the link to SwiftUI Documentation in an earlier answer
struct TextFieldTest: View {
#FocusState private var emailFocused: Bool
#FocusState private var passwordFocused: Bool
#State private var username: String = ""
#State private var password: String = ""
var body: some View {
TextField("User name (email address)", text: $username)
.focused($emailFocused)
.onSubmit {
passwordFocused = true
}
TextField("Enter Password", text: $password)
.focused($passwordFocused)
}
}
I created a view Modifier that takes in a binding of #FocusState. This will automatically handle the next progression and clear the keyboard.
import SwiftUI
struct KeyboardToolsView<Content: View, T: Hashable & CaseIterable & RawRepresentable>: View where T.RawValue == Int {
var focusedField: FocusState<T?>.Binding
let content: Content
init(focusedField: FocusState<T?>.Binding, #ViewBuilder content: () -> Content) {
self.focusedField = focusedField
self.content = content()
}
var body: some View {
content
.toolbar {
ToolbarItem(placement: .keyboard) {
HStack {
Button(action: previousFocus) {
Image(systemName: "chevron.up")
}
.disabled(!canSelectPreviousField)
Button(action: nextFocus) {
Image(systemName: "chevron.down")
}
.disabled(!canSelectNextField)
Spacer()
Button("Done") {
focusedField.wrappedValue = nil
}
}
}
}
}
var canSelectPreviousField: Bool {
if let currentFocus = focusedField.wrappedValue {
return currentFocus.rawValue > 0
} else {
return false
}
}
var canSelectNextField:Bool {
if let currentFocus = focusedField.wrappedValue {
return currentFocus.rawValue < T.allCases.count - 1
} else {
return false
}
}
func previousFocus() {
if canSelectPreviousField {
selectPreviousField()
}
}
func nextFocus() {
if canSelectNextField {
selectNextField()
}
}
func selectPreviousField() {
focusedField.wrappedValue = focusedField.wrappedValue.map {
T(rawValue: $0.rawValue - 1)!
}
}
func selectNextField() {
focusedField.wrappedValue = focusedField.wrappedValue.map {
T(rawValue: $0.rawValue + 1)!
}
}
}
struct KeyboardToolsViewModifier<T: Hashable & CaseIterable & RawRepresentable>: ViewModifier where T.RawValue == Int {
var focusedField: FocusState<T?>.Binding
func body(content: Content) -> some View {
KeyboardToolsView(focusedField: focusedField) {
content
}
}
}
extension View {
func keyboardTools<T: Hashable & CaseIterable & RawRepresentable>(focusedField: FocusState<T?>.Binding) -> some View where T.RawValue == Int {
self.modifier(KeyboardToolsViewModifier<T>(focusedField: focusedField))
}
}
Example of how it can be used:
struct TransactionForm: View {
#State private var price: Double?
#State private var titleText: String = ""
#State private var date: Date = .now
#FocusState private var focusedField: Field?
// Having an enum that is Int and CaseIterable is important. As it will allow the view modifier to properly choose the next focus item.
private enum Field: Int, CaseIterable {
case title, price
}
var body: some View {
NavigationView {
Form {
TextField("Title", text: $titleText)
.focused($focusedField, equals: .title)
TextField("$0.00", value: $price, format: .currency(code: "USD"))
.focused($focusedField, equals: .price)
.keyboardType(.decimalPad)
DatePicker("Date", selection: $date, displayedComponents: [.date])
Section {
Button(action: {...}) {
Text("Add")
}
}
.listRowBackground(Color.clear)
}
.navigationTitle("Add Transaction")
.keyboardTools(focusedField: $focusedField)
}
}
}

Resources