Checking the contents of a TextField variable method not working

Hoping someone can help me with this… The error is… Generic parameter ‘/‘ cannot be inferred.

                            .multilineTextAlignment(.center)
                            .onAppear(perform: {
                                var checkFirstCardLatitude = cards.firstCardLatitude
                                let charArray = Array(checkFirstCardLatitude)
                                let allowed: [Character] = ["-", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
                                for char in charArray {
                                    if char != allowed {
                                        cards.firstCardLatitude = "000.000000" // Reset Text Field
                                    }
                                }
                            })
Answered by darkpaw in 838112022

There is a slight issue with the regex, in that it seems to be allowing you to enter something like -12..sssds. It seems that as soon as you enter the second full-stop you can enter anything, so you may want to either check the regex, or work around it with some more code in the Coordinate.validate() function.

You don't say on which line is the error (are you sure it is in this part of code ?), but this appears wrong

 for char in charArray {
     if char != allowed {

char is a Character but allowed is probably an array of Character (you don't show much code, so we have to check, which is a bit boring).

Correct syntax would be (if I understand correctly your intent and if charArray is effectively an array of Character, which is likely as cards.firstCardLatitude is a String)

 for char in charArray {
     if !allowed.contains(char) {

If that works, don't forget to close the thread by marking the correct answer ; otherwise show more code.

/ is a forward slash.

Where does the error happen? Which line?

What is cards?

Also, char is a Character, and you're trying to compare one Character against an array of Character. Do you mean to use something like if (!allowed.contains(char)) {?

From what I can gather you're trying to make sure the value in the field is a valid latitude? There are better functions to do this.

You should create a Set of Character. From the documentation: You use a set instead of an array when you need to test efficiently for membership and you aren’t concerned with the order of the elements in the collection, or when you need to ensure that each element appears only once in a collection.

So, add this somewhere once only; you don't need to re-create this Set everywhere:

let validCoordCharacters: Set<Character> = ["-", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]

You're currently stripping out unwanted characters when the TextField first appears, but since it's an editable TextField you should be doing this when the value changes, too:

.onChange(of: theTextFieldText) {
	theTextFieldText.removeAll(where: { !validCoordCharacters.contains($0) } )
	}

And you will need to ensure that the minus character - is only ever at the beginning of the String for it to be a valid latitude/longitude. You could do this by checking if the String contains that character, and if its location in the String is not location 0, then you remove it from the String and add it to the beginning.

Moving this back into replies rather than comments, as you can more easily see replies.

Let us know exactly what you want your code to do. What are you trying to achieve?

You have a latitude text field, and in your original code you wanted to reset the value to "000.000000" if cards.firstCardLatitude had a character that isn't in your array?

Then what happens? The user can edit that field and enter anything they like because you didn't have any further code that would handle it. My code handles it.

As a user, when I'm typing in a field, if I were to accidentally hit the 't' character when I meant to press '5' ('t' is next to '5' on the keyboard) your code would reset the field; my code simply stops it being added to the string.

So, what do you want to achieve? Give us a few steps so we can help you a little more.

                        TextField("Latitude (May start with a minus and at least 1 number then a Full-Stop followed by up to 6 numbers)", text: $cards.firstCardLatitude)
                            .multilineTextAlignment(.center)
                            .onAppear(perform: {
                                let charArray = Array(cards.firstCardLatitude) // Convert Text Field to an Array of Characters? (“-“, “3”, “2”, “.”, “1”, “2”, “3”, “4”, “)”, ”6”) (between 4 and 6 “)” not allowed)
                                let allowed: [Character] = ["-", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] // Only these Characters are allowed
                                for char in charArray {  // Check each Character in Text Field input for unwanted Characters (only allowed are allowed)
                                if (!allowed.contains(char)) { // If an unwanted Character is present then reset Text Field to 000.000000
                                  cards.firstCardFirstWord = "000.000000" // When the Form is checked (next view) another trap looking for 000.000000 prevents the form from being submitted. User has to go back to that Card and try again.
                                    }
                                }
                            })

Okay, it looks like you should probably just use a Regex for this.

Here's a completely working example of a latitude and longitude that are being validated by a regex. I'll give you a little commentary after the code:

import SwiftUI

// Latitude: ^(\+|-)?(?:90(?:(?:\.0{1,6})?)|(?:[0-9]|[1-8][0-9])(?:(?:\.[0-9]{1,6})?))$
// Longitude: ^(\+|-)?(?:180(?:(?:\.0{1,6})?)|(?:[0-9]|[1-9][0-9]|1[0-7][0-9])(?:(?:\.[0-9]{1,6})?))$

let validCoordCharacters: Set<Character> = ["-", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]

struct ContentView: View {
	@State private var cards: card = .init(firstCardLatitude: "-12.345678", firstCardLongitude: "3.987654")
	@State private var latitudeResult: String = "No result"
	@State private var longitudeResult: String = "No result"

	var body: some View {

        VStack {
			Text("Latitude")
			TextField("Enter latitude", text: $cards.firstCardLatitude)
				.multilineTextAlignment(.center)
				.onAppear(perform: {
					if cards.firstCardLatitude.wholeMatch(of: /^(\+|-)?(?:90(?:(?:\.0{1,6})?)|(?:[0-9]|[1-8][0-9])(?:(?:\.[0-9]{1,6})?))$/) != nil {
						latitudeResult = "Match"

					} else {
						latitudeResult = "No match"
						// You can uncomment this line to reset the value, if you want
						//cards.firstCardLatitude = "000.000000"
					}

					cards.firstCardLatitude.removeAll(where: { !validCoordCharacters.contains($0) } )
				})
				.onChange(of: cards.firstCardLatitude) {
					cards.firstCardLatitude.removeAll(where: { !validCoordCharacters.contains($0) } )
				}
			Text("Result: \(latitudeResult)")

			Divider()

			Text("Longitude")
			TextField("Enter longitude", text: $cards.firstCardLongitude)
				.multilineTextAlignment(.center)
				.onAppear(perform: {
					if cards.firstCardLongitude.wholeMatch(of: /^(\+|-)?(?:180(?:(?:\.0{1,6})?)|(?:[0-9]|[1-9][0-9]|1[0-7][0-9])(?:(?:\.[0-9]{1,6})?))$/) != nil {
						longitudeResult = "Match"

					} else {
						longitudeResult = "No match"
						// You can uncomment this line to reset the value, if you want
						//cards.firstCardLongitude = "000.000000"
					}

					cards.firstCardLongitude.removeAll(where: { !validCoordCharacters.contains($0) } )
				})
				.onChange(of: cards.firstCardLongitude) {
					cards.firstCardLongitude.removeAll(where: { !validCoordCharacters.contains($0) } )
				}
			Text("Result: \(longitudeResult)")
        }
        .padding()
    }
}


struct card {
	var firstCardLatitude: String
	var firstCardLongitude: String
}


#Preview {
    ContentView()
}

Lines 3 and 4 are the regex for latitude and longitude, These are just comments. The actual regex is used on lines 20 and 42.

Line 6 is that Set<Character> that I explained previously. It is used on lines 32 and 54 to remove any invalid characters from the Strings when they are typed into the TextField.

Line 8 creates an instance of my simple cards struct with two vars in. Yours is likely more complex.

Lines 10 and 11 create State vars to hold a result. These are just for displaying on the screen in the preview so you can see the results of the validation against the regex.

Lines 19 and 41 run the regex when the TextFields appear. They check if the value in the card (line 9) matches the regex. If it does the result is set to "Match". If not, it's set to "No match". The preview is then updated to show you the result.

If you change the value in the preview (the iPhone display, not the code!) you'll find that you cannot enter any character other than those in the Set<> because lines 31 and 53 stop you doing that.

If, however, you change the values in line 9, you can see what happens. So, for example, if you changed the latitude to "-12.345xyz678" you won't see the "xyz" in the preview (lines 31 and 53 handle that), and you'll see the "No match" text.

If you uncomment lines 26 and 48 this will reset the values to "000.000000", which is what you want.

I strongly encourage you to read through this code and understand what it's doing. It's not difficult or magic; it's really very simple.

OMG! Thank you so very much! This is exactly what I love (more than solve my problem but actually the opportunity to learn). I will study this to fully understand it which will take some time I think. Having said that this means I probably shouldn’t mark it as solved unless you were able to test it? (Please advise).

darkpaw thank you for taking the time to help me with this… Your code works perfectly but unfortunately not when inserted in to my app. I have copied and pasted my attempt to get it to work and have added some more of my code to hopefully better explain. I understand it may not work due to some code & variables missing and is only for explanation purposes. Thank you again for your help so far and I am hoping you can find the time and patience to help a “newbie” despite being elderly!

import SwiftUI
// Latitude: ^(\+|-)?(?:90(?:(?:\.0{1,6})?)|(?:[0-9]|[1-8][0-9])(?:(?:\.[0-9]{1,6})?))$
// Longitude: ^(\+|-)?(?:180(?:(?:\.0{1,6})?)|(?:[0-9]|[1-9][0-9]|1[0-7][0-9])(?:(?:\.[0-9]{1,6})?))$
var scanArray: [String] = [] // From my app
var gameArray: [String] = [] // From my app
 // From my app
private var emptyData =  "1,,00.000000,00.000000,,,,,,,,2,,00.000000,00.000000,,,,,,,,3,,00.000000,00.000000,,,,,,,,4,,00.000000,00.000000,,,,,,,,5,,00.000000,00.000000,,,,,,,,6,,00.000000,00.000000,,,,,,"
// See func bottom of Struct that builds the gameArray below
class Cards: ObservableObject { // From my app
 @Published var firstCardLatitude = gameArray [2]
 @Published var firstCardLongitude = gameArray [3]
    // etc. etc.
 @Published var latitudeFormatIncorrect = "Latitude (May start with a minus and at least 1 number then a Full-Stop followed by up to 6 numbers)"
 @Published var longitudeFormatIncorrect = "Longitude (May start with a minus and at least 1 number then a Full-Stop followed by up to 6 numbers)"
 }
let validCoordCharacters: Set<Character> = ["-", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
struct ContentView: View {
    @StateObject var cards = Cards() // From my app
    //@State private var cards: card = .init(firstCardLatitude: "-12.345678", firstCardLongitude: "3.987654") // Error "Cannot find card in scope" I understand this is because your Struct card { is commented out. If I comment this out the error disappears but with no errors Playgrounds Crashes during the Build.
    @State private var latitudeResult: String = "No result"
    @State private var longitudeResult: String = "No result"
    var body: some View {
        VStack { // From my app
            Spacer()
                .onAppear(perform: {
                    buildScan()
                })
        }
        VStack {
            Text("Latitude")
            TextField("Enter latitude", text: $cards.firstCardLatitude)
                .multilineTextAlignment(.center)
                .onAppear(perform: {
                    if cards.firstCardLatitude.wholeMatch(of: /^(\+|-)?(?:90(?:(?:\.0{1,6})?)|(?:[0-9]|[1-8][0-9])(?:(?:\.[0-9]{1,6})?))$/) != nil {
                        latitudeResult = "Match"
                        // cards.firstCardLatitude = cards.firstCardLatitude
                    } else {
                        latitudeResult = "No match"
                        // You can uncomment this line to reset the value, if you want
                        // cards.firstCardLatitude = "000.000000"
                        // cards.firstCardLatitude = latitudeFormatIncorrect
                    }
                    cards.firstCardLatitude.removeAll(where: { !validCoordCharacters.contains($0) } )
                })
                .onChange(of: cards.firstCardLatitude) {
                    cards.firstCardLatitude.removeAll(where: { !validCoordCharacters.contains($0) } )
                }
            Text("Result: \(latitudeResult)")
            
            Divider()
            
            Text("Longitude")
            TextField("Enter longitude", text: $cards.firstCardLongitude)
                .multilineTextAlignment(.center)
                .onAppear(perform: {
                    if cards.firstCardLongitude.wholeMatch(of: /^(\+|-)?(?:180(?:(?:\.0{1,6})?)|(?:[0-9]|[1-9][0-9]|1[0-7][0-9])(?:(?:\.[0-9]{1,6})?))$/) != nil {
                        longitudeResult = "Match"
                    } else {
                        longitudeResult = "No match"
                        // You can uncomment this line to reset the value, if you want
                        // cards.firstCardLongitude = "000.000000"
                        // cards.firstCardLongitude = longitudeFormatIncorrect
                    }
                    cards.firstCardLongitude.removeAll(where: { !validCoordCharacters.contains($0) } )
                })
                .onChange(of: cards.firstCardLongitude) {
                    cards.firstCardLongitude.removeAll(where: { !validCoordCharacters.contains($0) } )
                }
            Text("Result: \(longitudeResult)")
        }
        .padding()
    }
}
func buildScan() { // From my app
    scanArray = emptyData.components(separatedBy: ",")
    gameArray = scanArray
}

/*struct card { // I Want to replace this with what I have ( class Cards: ObservableObject { )
    var firstCardLatitude: String
    var firstCardLongitude: String
}*/

/*#Preview {
    ContentView()
}*/

I think it's crashing because you're trying to access something that hasn't yet been created.

Your Cards class is being created before your UI, but it depends on the stuff being done inside buildScan() which is only being executed when the UI is drawn.

I'm just working on updated code for you. It will need some explanation...

Okay, here we go. This answer will be done in a few posts:

  • I've created a Card class, which is where you store the latitude and longitude of the card, and I've added in a couple of other properties so you can see how classes are used.

  • Card is an ObservableObject with @Published variables.

  • It also contains a static let, which is simply a way of letting you access something in a class without having to create an instance of that class. In this example I've added a static let emptyCard which is an instance of Card with default values.

  • You access it with Card.emptycard. You see how you don't access it via an instance of the class? You don't call for emptyCard on a card you've already created. So, you don't do: var myCard: Card = <create the card> followed by myCard.emptyCard. In fact, you cannot do that; it's not possible to access it that way.

  • I've also created a Coordinate class which contains the latitude and longitude for a coordinate.

  • I separated this out because it's a learning exercise for you, but it also makes sense because a coordinate is its own special thing with its own special properties and functions.

  • So... in the Coordinate class there's a static let defaultCoord which can be accessed with Coordinate.defaultCoord.

  • There's a static let zeroCoord which is an instance of Coordinate with the default values. It's used in Card.emptyCard.

  • I've moved the allowed characters Set<Character> here because it made sense. It's used by Coordinates so it makes sense that Coordinate contains that value.

  • The same applies for your error message: static let formatIncorrect: String = "May start with a minus and at least 1 number, then a full-stop followed by up to 6 numbers".

  • Then there's a static public func validate(_ coord: String) -> String { which validates a coordinate by removing unwanted characters, and making sure there's only one minus sign.

  • In the main app - the bit where your app launches from (marked by @main), I've added an init() function that runs your buildScan() function, and also added a function createData() that creates two Cards to use in the code.

  • Finally the ContentView, now that the cards are available, it can properly access them and use them in the UI.

  • I moved the duplicated UI code into its own View struct, and you simply supply whether you're doing the latitude or longitude. The code in the View draws appropriately.

//  MainApp.swift

import SwiftUI

@main
struct MainApp: App {
	init() {
		// The init() function runs when the app starts, so these two functions are executed before the UI is created
		buildScan()
		createData()
	}

	var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

// MARK: - Global vars

var gameArray: [String] = []
var cards: [Card] = []  // An array of your Card class

// MARK: - Startup functions

func buildScan() {
	let emptyData =  "1,,00.000000,00.000000,,,,,,,,2,,00.000000,00.000000,,,,,,,,3,,00.000000,00.000000,,,,,,,,4,,00.000000,00.000000,,,,,,,,5,,00.000000,00.000000,,,,,,,,6,,00.000000,00.000000,,,,,,"
	gameArray = emptyData.components(separatedBy: ",")
}

func createData() {
	// Two instances of the Card class are created here and added to the `cards` array. Note the name and city values; they're just example data.
	cards.append(Card.init(coord: Coordinate(latitude: gameArray[2], longitude: gameArray[3]), name: "Card1", city: "London"))
	cards.append(Card.init(coord: Coordinate(latitude: gameArray[2], longitude: gameArray[3]), name: "Card2", city: "San Francisco"))
}
//  Card.swift

import Foundation

/*
 This is your Card class.

 It contains an instance of a Coordinate class that contains the latitude and longitude for a card.
 It also contains a few example variables to show you how a class can be used.
 */

class Card: ObservableObject {
	@Published var coord: Coordinate
	@Published var name: String
	@Published var city: String

	init(coord: Coordinate, name: String, city: String) {
		self.coord = coord
		self.name = name
		self.city = city
	}

	/*
	 This allows you to create an empty card, i.e. one with default values. You can do this in any of the following ways:
	 `var newCard: Card = Card.emptyCard`
	 `var newCard: Card = .emptyCard`
	 `var newCard = Card.emptyCard`
	 */
	static let emptyCard = Card(coord: Coordinate.zeroCoord, name: "", city: "")
}
//  Coordinate.swift

class Coordinate {
	var latitude: String
	var longitude: String

	init(latitude: String, longitude: String) {
		self.latitude = latitude
		self.longitude = longitude
	}

	static let defaultCoord: String = "000.000000"
	static let zeroCoord: Coordinate = .init(latitude: defaultCoord, longitude: defaultCoord)

	// Moved these into this class as they only need to be created once, and can be accessed via the class itself, i.e. Coordinate.validChars
	static let validChars: Set<Character> = ["-", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
	static let formatIncorrect: String = "May start with a minus and at least 1 number, then a full-stop followed by up to 6 numbers"

	// This function validates a coordinate by removing unwanted characters, and making sure there's only one minus sign
	static public func validate(_ coord: String) -> String {
		var coord_ = coord
		// Remove invalid characters
		coord_.removeAll(where: { !validChars.contains($0) } )

		// If there's more than one minus sign, remove them all and put one at the beginning
		// This handles a value like "-12.34-56", transforming it to "-12.3456"
		if(coord_.filter( { $0 == "-" } ).count > 1) {
			coord_.removeAll(where: { "-".contains($0) } )
			coord_ = "-" + coord_
		}

		// If there's a minus sign, and the first occurrence of it isn't at the beginning
		// This handles a value like "12.34-56", transforming it to "-12.3456"
		if(coord_.filter( { $0 == "-" } ).count > 0 && coord_.firstIndex(of: "-")! > coord_.startIndex) {
			// Remove all the minus signs, and add one to the beginning
			coord_.removeAll(where: { "-".contains($0) } )
			coord_ = "-" + coord_
		}

		return coord_
	}
}
// ContentView.swift

import SwiftUI

struct ContentView: View {
	// This gets the first card where the name property is "Card1"
	@ObservedObject private var card: Card = cards.first(where: { $0.name == "Card1" }) ?? Card.emptyCard

	var body: some View {
		VStack {
			// In order to reduce duplicated code, I moved the TextField and related stuff into its own View struct
			CoordEntryView(isLatitude: true, card: card)
			Divider()
			CoordEntryView(isLatitude: false, card: card)
		}
		.padding()
	}
}

struct CoordEntryView: View {
	var isLatitude: Bool  // Allows us to use this same code for the two different coorindates
	@ObservedObject var card: Card

	@State private var valueOk: Bool = true

	var body: some View {
		Text(isLatitude ? "Latitude" : "Longitude")
			.bold()
		TextField("Enter value", text: (isLatitude ? $card.coord.latitude : $card.coord.longitude))
			.multilineTextAlignment(.center)
			.font(.system(size: 36))
			.frame(maxWidth: 300)
			.background(
				RoundedRectangle(cornerRadius: 8)
					.fill(Color.blue.opacity(0.15))
			)
			.onAppear(perform: {
				checkCoords()
			})
			.onChange(of: (isLatitude ? card.coord.latitude : card.coord.longitude)) {
				checkCoords()
			}
		if(!valueOk) {
			Text(Coordinate.formatIncorrect)
				.foregroundStyle(.red)
		}
	}

	private func checkCoords() {
		// Checks the coordinate value is valid, removes irrelevant characters, makes sure the minus sign is at the beginning, and trims to the right length
		if(isLatitude) {
			valueOk = (card.coord.latitude.wholeMatch(of: /^(\+|-)?(?:90(?:(?:\.0{1,6})?)|(?:[0-9]|[1-8][0-9])(?:(?:\.[0-9]{1,6})?))$/) != nil)
			card.coord.latitude = Coordinate.validate(card.coord.latitude)

			// Trim to the right length
			card.coord.latitude = String(card.coord.latitude.prefix((card.coord.latitude.prefix(1) == "-" ? 10 : 9)))

		} else {
			valueOk = (card.coord.longitude.wholeMatch(of: /^(\+|-)?(?:180(?:(?:\.0{1,6})?)|(?:[0-9]|[1-9][0-9]|1[0-7][0-9])(?:(?:\.[0-9]{1,6})?))$/) != nil)
			card.coord.longitude = Coordinate.validate(card.coord.longitude)

			// Trim to the right length
			card.coord.longitude = String(card.coord.longitude.prefix((card.coord.longitude.prefix(1) == "-" ? 11 : 10)))
		}
	}
}

#Preview {
	ContentView()
}
Accepted Answer

There is a slight issue with the regex, in that it seems to be allowing you to enter something like -12..sssds. It seems that as soon as you enter the second full-stop you can enter anything, so you may want to either check the regex, or work around it with some more code in the Coordinate.validate() function.

darkpaw thank you very much for taking the time with this. I will treat it as a learning opportunity to start with but won’t attempt to use it in my app until I understand it and feel confident to do so. Thanks again, much appreciated! P.S. I have created an app in Playgrounds using your code and it works okay.

Checking the contents of a TextField variable method not working
 
 
Q