Bronze Challenge

At first I was not entirely happy with this; it seemed like overkill to turn the newly typed character into a set, but I didn’t see a direct way of checking to see if an individual character was a member of a set. (Although with a bit more digging I found the contains method which would do the trick.)

But if you take cut & paste into account, you could have multiple characters being inserted all at the same time, so turning the replacement string into a set makes more sense in that light.

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool
{
    let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
    let replacementTextHasDecimalSeparator = string.range(of: ".")
        
    let replacementTextCharacterSet = CharacterSet(charactersIn: string)
    let replacementTextIsAlphabetic = (replacementTextCharacterSet.isDisjoint(with: CharacterSet.letters) == false)
        
    if (existingTextHasDecimalSeparator != nil &&
        replacementTextHasDecimalSeparator != nil) ||
        replacementTextIsAlphabetic
    {
        return false
    }
    else
    {
        return true
    }
}

I did this a bit differently, without touching the existing checks for the double decimal points, I used a guard at the start of the textField(_:shouldChangeCharactersIn:replacementString:), as follows:

var numbers = CharacterSet.decimalDigits
numbers.insert(charactersIn: ".")
        
guard (string.rangeOfCharacter(from: numbers) != nil) || string.isEmpty else {
    return false
}

I created a CharacterSet of decimal digits (i.e. numbers only) and then also added the “.” to it to make it allowed, too. Then checked for the range of the first character found in the replacementString from the character set. If this is not nil, meaning it did find a valid numeric digit or “.”, OR if the replacementString is empty (which means the user is trying to delete numbers), then continue with the rest of the function, otherwise return false and prevent changes.

The “letters” group from CharacterSet does not include other symbols such as / [ ] ', etc. and these appear to be valid inputs in your solution.

The “letters” group from CharacterSet does not include other symbols such as / [ ] ', etc. and these appear to be valid inputs in your solution.

True. The challenge was to prohibit alphabetic characters so I chose to do only that, and not worry about symbols or other non-alphanumeric characters.

Also, there’s a problem with your implementation that you’re going to find out about in the next chapter.

1 Like

Spent several hours racking my brain on this one and reading additional articles involving characterSets, and it almost felt like I stumbled upon the solution I got which prevents letters like Jon pointed out from being valid. Is there any underlying issue with it in regards to other ways to go about it?

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

    let existingTextHasDecimalSeparator = textField.text?.range(of:".")
    let replacementTexthasDecimalSeparator = string.range(of: ".")
    // character set containing numbers
    let numbers = CharacterSet.decimalDigits
    // character set containing letters
    let letters = CharacterSet.letters
    
    let currentText = textField.text?.rangeOfCharacter(from: numbers)
    let replacementText = string.rangeOfCharacter(from: letters)
   
    
    // if existingTextHasdecimalSeparator does not equal 0 and replacementTexthasDecimalSepartor does not equal 0, or currentText does not equal 0 and replacmentText does not equel 0 reject the change. Otherwise go ahead with the change
    if existingTextHasDecimalSeparator != nil && replacementTexthasDecimalSeparator != nil || currentText != nil && replacementText != nil
    {
        return false
    }else {
        return true
    }

I did this, which seems to work

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
    let replacementTextHasDecimalSeparator = string.range(of: ".")
    
    guard string == "." || Double(string) != nil else { return false }
    
    if (existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil) {
        return false
    } else {
        return true
    }
}
1 Like

Here is my solution

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        let existingTextHasDecimalSeperator = textField.text?.range(of: ".")
        let replacementTextHasDecimalSeperator = string.range(of: ".")
        
        // Bronze chapter 6 challenge to not allow letters. I also made it not allow symbols
        var disallowed = CharacterSet.letters
        disallowed.insert(charactersIn: "!@#$%&*(){}/\"\\';:,><|[]-=_+")
        var replacementCharSet = CharacterSet()
        replacementCharSet.insert(charactersIn: string)
        let badChar = replacementCharSet.isSubset(of: disallowed)

        if existingTextHasDecimalSeperator != nil && replacementTextHasDecimalSeperator != nil || badChar && !string.isEmpty {
            return false
        } else {
            return true
        }
    }

I had to make sure that that if the replacement string was empty it would still be allowed because otherwise you could not click delete because delete sends an empty string to the function. I also made it do it ignores symbols except the period

Helpful link: CharacterSet - NSHipster

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {
    
    let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
    let replacementTextHasDecimalSeparator = string.range(of: ".")
    
    let invalidChars = string.rangeOfCharacter(from: CharacterSet.letters)

    if (existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil) || invalidChars != nil {
        return false
    } else {
        return true
    }

}

Why did this code work? It’s literally just 1 line and an added option in the if statement.

EDIT: I guess I just don’t understand this part very well:
let existingTextHasDecimalSeparator = textField.text?.range(of: “.”)
let replacementTextHasDecimalSeparator = string.range(of: “.”).
I made it work just by pure luck

Those are both pretty simple. The range(of:) function checks to see if a string occurs within another string, and if the string is found it returns the location of the string, otherwise it returns nil; in this case it’s checking to see if the character “.” occurs within either the existing text in the UITextField (the first line) or the new text that’s being added to it (the second line).

The new line you added does something similar, except instead of searching for a string it searches for any character in the provided character set. CharacterSet.letters is a list of characters that qualify as letters in various languages. (I originally wrote that it was just a-z but apparently it’s more comprehensive than that. But for the purposes of North American English it would basically be a-z in lower & upper case.)

So if invalidChars is not nil that means it found a letter somewhere in the string being entered into the UITextField and that makes it an invalid temperature value so don’t allow it.

1 Like