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.

No problem. I hope it helps you to learn a little more Swift and SwiftUI. I've given you a few extra concepts that you might be able to use in your own code.

I think the important ones are:

  • Running functions when an app starts: init() within your @main entry point.
  • Using classes and splitting them out where appropriate: splitting Coordinate into its own class.
  • Putting things that are specific to a class within the class itself: the validate() function in Coordinate.
  • Trying to cover all possibilities when you're working on something: the different ways we handle the minus sign in the validate() function.
  • Reusing code: creating the CoordEntryView struct that we can reuse for both latitude and longitude.

I am by no means a genius with Swift and SwiftUI. I get things wrong, and I still have to ask questions of others. Don't be afraid to ask further questions on these forums. We're quite nice people!

Good luck with your code!

I am not familiar with this method of Form making and am not sure how to prevent this doubling up. Can you help please? Also I don’t know how to call the second card to follow the first and the other subsequent cards with this method?

Well, I don't know what code you're using to create that, but it seems that you're displaying the same card each time? If you're using my code it does say it uses the first card where the name is "Card1", so you'll want to pass in the correct value.

Apologies for experimenting with your Code but I was trying to add more Text (Card Number) and TextFields etc. contained in a Card. I was under the impression that your View that contained the Latitude and Longitude TextFields could be added to with other fields ? I have added a TextField to your Code without further modification and the result is the same. Again thank you and do understand if you don’t want to assist me any further.

// 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 { // Added this TextField to your Code
        TextField("Name", text: ($card.name))
            .multilineTextAlignment(.center)
            .font(.system(size: 36))
            .frame(maxWidth: 300)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(Color.blue.opacity(0.15))
            )
        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()
}

Okay, I see what you're doing.

The CoordEntryView is to be used solely for entering coordinates.

You should create a separate view that deals with different input requirements.

I've changed the Card class to rename city, which was just an example variable, to threeWords:

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

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

	/*
	 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: "", threeWords: "")
}

Then, because it's used in createData() in the main app:

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", threeWords: "london.is.great"))
	cards.append(Card.init(coord: Coordinate(latitude: gameArray[2], longitude: gameArray[3]), name: "Card2", threeWords: "cheese.is.yummy"))
}

Then, in the ContentView I've added a new struct:

struct TextEntryView_ThreeWords: View {
	@ObservedObject var card: Card
	@State private var valueOk: Bool = true

	var body: some View {
		TextField("Enter value", text: $card.threeWords)
			.multilineTextAlignment(.center)
			.font(.system(size: 36))
			.frame(maxWidth: 300)
			.background(
				RoundedRectangle(cornerRadius: 8)
					.fill(Color.blue.opacity(0.15))
			)
			.onAppear(perform: {
				checkValue()
			})
			.onChange(of: card.threeWords) {
				checkValue()
			}
		if(!valueOk) {
			Text("3 lowercase words separated with a full-stop, e.g.: monkey.cheesecake.button")
				.foregroundStyle(.red)
		}
	}

	private func checkValue() {
		// Lowercase the String
		card.threeWords = card.threeWords.lowercased()
		// This line splits the value entered into an Array of Strings
		let entered = card.threeWords.lowercased().split{ $0 == "." }.map(String.init)
		// And this line simply checks whether there are three elements (words) in the array
		valueOk = (entered.count == 3)
	}
}

And to use it, I've added it into the VStack like this:

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)
			Divider()
			TextEntryView_ThreeWords(card: card)
		}
		.padding()

And it looks like this:

Thanks again, very much appreciated! On first glance you have done what I first tried (creating another Struct etc.) but it didn’t work for me. I will compare yours with mine and hopefully see where I went wrong. I was thinking about posting what I tried prior to my previous post which produced the doubling up for you to see if I am making tracks in the right direction but not sure if it would be appropriate here?

This is the piece I was missing! Thank you! I still think I have much to learn but thanks to you I am making progress!

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)
			Divider()
			TextEntryView_ThreeWords(card: card)
		}
		.padding()

Is it possible to continue on with the next card/s in the same view with this method? I have created another Struct but not sure how to populate it with the Second Card?

It's a little more complicated to explain, but basing everything off the code I've already provided... you have your cards array in the main app:

func createData() {
	// Two instances of the Card class are created here and added to the `cards` array
	cards.append(Card.init(coord: Coordinate(latitude: gameArray[2], longitude: gameArray[3]), name: "Card1", threeWords: "london.is.great"))
	cards.append(Card.init(coord: Coordinate(latitude: gameArray[2], longitude: gameArray[3]), name: "Card2", threeWords: "cheese.is.yummy"))
}

That's where your data is set up. It contains two cards right now, but you can expand it however you want.

Now you need to loop through those cards so you can add them to the UI, so in your ContentView find this line towards the top:

@ObservedObject private var card: Card = cards.first(where: { $0.name == "Card1" }) ?? Card.emptyCard

You don't need it anymore so you can delete it, or you can leave it there for reference and simply comment it out if you like.

Change your VStack to this:

ForEach(cards, id: \.self) { card in
			VStack {
				CoordEntryView(isLatitude: true, card: card)
				Divider()
				CoordEntryView(isLatitude: false, card: card)
				Divider()
				TextEntryView_ThreeWords(card: card)
			}
			.padding()
		}

What you're doing there is putting the VStack inside a ForEach loop. ForEach simply runs through the items in the cards array, and each item is then referred to as card (where it says card in).

In order to use ForEach, your Card class has to be changed to be Hashable. It is changed to become this:

class Card: ObservableObject, Hashable {  // Note: Hashable added here
	var uid = UUID().uuidString
	@Published var coord: Coordinate
	@Published var name: String
	@Published var threeWords: String

	static func == (lhs: Card, rhs: Card) -> Bool {
		return lhs.uid == rhs.uid
	}

	func hash(into hasher: inout Hasher) {
		hasher.combine(uid)
	}

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

	/*
	 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: "", threeWords: "")
}

Basically, you add a unique identifier: var uid = UUID().uuidString and you add a couple of methods that make it hashable.

Now, when you run the app, you should see both cards in the UI:

Thank you again but yet again out of my depth! Having said that I feel I have learned so much from your help! I am thinking when my head stops spinning I will compare the difference/s between how you have constructed this Form and how I have in my App. I already know your way has far less need for Variables. PS I added ScrollView to make the View scrollable.

var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            VStack {
                ForEach(cards, id: \.self) { card in
                    VStack {
                        Divider()
                        TextEntryView_PartOne(card: card)
                        Divider()
                        CoordEntryView(isLatitude: true, card: card)
                        Divider()
                        CoordEntryView(isLatitude: false, card: card)
                        Divider()
                        TextEntryView_PartTwo(card: card)
                        Divider()
                    }
                    .padding()
                }
            }
        }
        .padding()
    }

Thanks again for your help with this. Although I have not used your solution exactly it did help me a great deal to construct a solution (example enclosed).

  • Still working on these examples of input error…

-.234567 (If (Left of “.”) contains “-“ then the length of (Left of “.” ) minimum = 2).

1-2.123456 (If (Left of “.”) contains “-“ it should be the first character.

I understand your solution is far better because it reduces the amount of code etc. but I am not ready to rebuild my app from the ground-up yet. I need to learn and understand Swift alot more before I tackle that.

// In this example “coordinate” is either Latitude or Longitude
// Error example = 00.000000

// If coordinate contains more than one  “-“ 
if(coordinate.filter( { $0 == "-" } ).count > 1) {
            coordinate = "00.000000" // Reset coordinate
            }

// If coordinate contains more than one  “.“
if(coordinate.filter( { $0 == "." } ).count != 1) {
                coordinate = "00.000000" // Reset coordinate
            }

// If coordinate contains an illegal character
// Get coordinate length before check
let coordinateLengthBefore = coordinate.count 
let validCharsCoordinate: Set<Character> = ["-", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] // Allowed characters
// Remove characters not in Character Set
coordinate.removeAll(where: { !validCharsCoordinate.contains($0) } )
// Get coordinate length after check
let coordinateLengthAfter = coordinate.count
// If coordinate length is different due to illegal character removal
        if coordinateLengthBefore != coordinateLengthAfter {
                coordinate = "00.000000" // Reset coordinate
            }

// Check each side of the "."
// Check coordinate before the "."
        let coordinateStr = coordinate
        let coordinateCh = Character(".")
        let coordinateResult = coordinateStr.split(separator: coordinateCh).map         { String($0) }
        let coordinateLeftSplit = coordinateResult[0]
        let coordinateLeftCount = coordinateLeftSplit.count
        if coordinateLeftCount >= 4 { // More than 3 characters before "."
cards.firstCardLatitude = "00.000000" // Reset coordinate
            }
// Check coordinate after the "."
        let coordinateRightSplit = coordinateResult[1]
if(coordinateRightSplit.filter( { $0 == "-" } ).count > 0) {
                coordinate = "00.000000" // Reset coordinate ("-" not allowed)
            }
Checking the contents of a TextField variable method not working
 
 
Q