Bronze Challenge - Can I get some help understanding this?


#1

So towards the end of Chapter 4 we make our view controller a delegate, make it conform to a protocol, then run a method from the protocol.
We ended up preventing any input from the user that would cause there to be more than one decimal place in the text field.

The challenge wants us to prevent any non-numeric characters from being inputted via a bluetooth keyboard.
I studied the code and I got the nuts and bolts down, and I looked into NSCharactersetSet to help me take on the challenge, and I’m having trouble getting it off the ground (even though I have some idea of what I’d like to do)

Here is what I’m trying to do:

  1. Create an instance of NSCharacterSet.
  1. Once the object is created, run letterCharacterSet() method on it.

I have some ideas of what to do once I’m able to run this function, but problem is I’m unable to use the function on the object. Xcode doesn’t list it as an available function to hasAlphabetCharacter.
I don’t think I am setting this whole thing up correctly, which is making me second guess my understanding of classes and creating instances of them. Am I overlooking something basic?


#2

Sorry for double posting, but I think I figured it out. I was not creating an instance of the class with the code I was typing… Sorry group, I’m still new to programming in general, so it has been a struggle taking in all these things. :blush:


#3

You can use String’s method:rangeofCharacterFromSet.


#4

I found a solution that works but now I can’t delete previous characters with iOS keyboard or my own keyboard.

gist.github.com/BudaDude/93f3449a30aab5f9862a
(Using gist so no one gets spoiled for how to do it)

Could anyone give me an idea of what went wrong?


#5

I added changes in the comments.

extension ConversionViewController : UITextFieldDelegate {
    func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange,
                   replacementString string: String) -> Bool {
        let existingText = textField.text?.rangeOfString(".")
        let replacementText = string.rangeOfString(".")
        let digitSet = NSCharacterSet.decimalDigitCharacterSet()

        if existingText != nil && replacementText != nil {
            return false
        }
        
        for char in string.unicodeScalars {
            if char == "." {
                continue
            }
            if digitSet.longCharacterIsMember(char.value) == false {
                return false
            }
        }
        
    
        return true
    }

}

Its not perfect and doesn’t handle backspaces etc, its a start.


#6

Surely, we’re missing something obvious? The solution using unicode scalars seems needlessly complex for this stage in the book.


#7

This was my solution

[code] func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
print(“Current text: (textField.text)”)
print(“Replacement text: (string)”)

    let existingTextHasDecimalSeparator = textField.text?.rangeOfString(".")
    let replacementTextHasDecimalSeparator = string.rangeOfString(".")
    let letters = NSCharacterSet.letterCharacterSet()
    
    if string.lowercaseString.rangeOfCharacterFromSet(letters) != nil {
        return false
    }
        
    else {
        
        if existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil {
            return false
        }
        else
        {
            return true
        }
    }
}[/code]

#8

[quote=“budadude”]I found a solution that works but now I can’t delete previous characters with iOS keyboard or my own keyboard.

gist.github.com/BudaDude/93f3449a30aab5f9862a
(Using gist so no one gets spoiled for how to do it)

Could anyone give me an idea of what went wrong?[/quote]

As a backspace will return a zero-length string, you can ignore it by adding a count of the returned string length to the if statement.

Here’s my solution (although your solution of defining a new character set is much more elegant :slight_smile:)

https://gist.github.com/dpauk/2c4defdea366fe14044b


#9

@emanleet,

I don’t envy you. GUI programming is an intermediate to advanced topic, so be prepared to struggle. In addition, Big Nerd books often show a code listing with zero explanation. In my opinion, that means their books are not very good because I believe a computer programming book should be completely self explanatory.

In any case, you typically have to do a lot of digging through the docs, and that is especially difficult for a beginner to Swift iOS programming because the docs often refer to Objective-C, which is an entirely different language. In addition, the error messages in the console often mention Objective-C code because your Swift code is calling pre-existing code written in Objective-C–that’s what all that NSClassNameHere stuff is all about. (By the way, NS stands for NextStep, which was an operating system created by Next Computing, which was the company that Steve Jobs founded after he was kicked out of Apple. Apple later acquired Next Computing.)

At the very least it would behoove you to know this Objective-C syntax:

That is the syntax for calling a function in Objective-C. In Swift, that is equivalent to:

which could come from a Swift class defined something like this:


class Dog {
    func showNum(num: Int, andString: String) {
        print("The args were: \(num) and \(andString)")
    }
}

let d = Dog()
d.showNum(10, andString:"Hello")

--output:--
"The args were: 10 and Hello\n"

This book has started to do the zero explanation thing on p.74 where there is no explanation of the syntax:

Anyone who wants to know what that syntax means should read the apple docs on “optional chaining” here:

developer.apple.com/library/ios … CH21-ID245

Another example of a code listing with no explanation is the “closure” syntax on p.75. The book says that we will learn more about closures in chapter 15, but: 1) Chapter 15 is just too far away, and 2) In chapter 15, there is only a short paragraph of explanation, which is itself inadequate–and which could have easily fit in the current chapter in the blank space at the bottom of p. 75, 77, or 79. I would have much rather learned about the closure syntax now than the mostly irrelevant section in the current chapter titled “More on Protocols” on p.79.

I imagine you probably were confused by the term “closure”. Apparently, in Swift you can create anonymous functions, which are functions without a name (and in this case without arguments) like this:

[code]let myFunc = {return 8 * 42}

let val = myFunc() //=> 336
[/code]

Now, why would you want to do that instead of writing this:

[code]func myFunc() {
return 8 * 42

let val = myFunc() //=> 336
[/code]

If you only need to use a function once, it can be convenient to omit the formal function definition:

[code]let val = {
return 8*42
}() <= execute the anonymous function immediately

//Here the function no longer exists
[/code]

The term “closure” means that an anonymous function can see the variables outside the function:

[code]let x = 5
let myFunc = {return 8 * x}

myFunc() //=>40[/code]

It is said that the anonymous function “closes” over the variables outside the function. Therefore, when you see the term ‘closure’, think “anonymous function that can see the variables outside the function”.


#10

@douglance

That’s sort of the natural path that you are led down. Based on the hint, you should use an NSCharacterSet, and once you get that sorted out, the next question is: how do you determine if a character in the textfield’s replacement text is in the NSCharacterSet? So, after digging around in the docs you might end up with something like this:

for char in replacementString.characters { if letters.characterIsMember(char) { return false //Found a letter, which we are trying to prevent, so //tell the textfield it should NOT allow the replacement } }

But, that produces the error:

error: cannot convert value of type 'Character' to expected argument type 'unichar' (aka 'UInt16') if letters.characterIsMember(char) { ^~~~

In Xcode, if you Option+click on the variable name char, you will see that it is of type Character, and if you option click on the method characterIsMember, you will be led to the function signature:

Therefore, you have to figure out how to convert a Character type to a unichar type. I got lost in the docs trying to figure out how to do that, so I tacked and changed course. Eventually, I stumbled on the same method as JusticeBao: the String class’s rangeofCharacterFromSet(:slight_smile: method. The rangeOf methods in the String class have confusing names. They return some type of Range object, but what’s in the Range object? The docs say that rangeofCharacterFromSet(:):

What does “the range of the first character” mean? To figure that out, I used the following code in a playground:

[code]let replacementText = “789abc:"
let letters = NSCharacterSet.letterCharacterSet()

let range = replacementText.rangeOfCharacterFromSet(letters)

if let valid_range = range {
for x in valid_range {
print(x)
}

–output:–
3[/code]

Apparently, rangeOfCharacterFromSet() returns a Range object that contains just one value: the index value of the first matching character(or nil if there’s no match):

012345 - index value
789abc - string
   ^
   |

One value in a Range object? That is a bit confusing. I guess you can create a one value Range object:

[code]

let range = 3…<4

for i in range {
print(i)
}

–output:–
3[/code]

For the Range syntax, look in the Basic Operators section here: developer.apple.com/library/ios … 7-CH6-ID60

Therefore, the docs could be made clearer by stating that rangeOfCharacterFromSet():


#11

@DaVinci,

You don’t need the .lowercaseString here:

let letters = NSCharacterSet.letterCharacterSet()

 if string.lowercaseString.rangeOfCharacterFromSet(letters) != nil 

Is that some kind of optimization? That also made me wonder if all unicode characters have a lowercase version. I would guess not. But lowercaseString does not return an Optional type, so lowercaseString doesn’t return nil if it can’t convert to lowercase. Maybe if there’s isn’t a lowercase version of the character, lowercaseString leaves the character unchanged?

Also, you don’t need the outer else here:

if string.lowercaseString.rangeOfCharacterFromSet(letters) != nil {
    return false
}
    
else {
    
    if existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil {
        return false
    }
    else
    {
        return true
    }
}

Compare to:

[code]if string.lowercaseString.rangeOfCharacterFromSet(letters) != nil {
return false
}

if existingTextHasDecimalSeparator != nil
&& replacementTextHasDecimalSeparator != nil {
return false
}
else
{
return true
}
[/code]
You could even remove the else there, too.

Note that although your solution meets the requirements of the exercise, you can still use the hardware keyboard to type in characters like ! # % *. I solved that problem with regexes:


       //This comes after the test for a decimal point:

        let match = string.rangeOfString(
                "^\\d|.$",      //Specify a regex here (you have to escape all backslashes). 
                                //This regex says to match any digit or a period.
                options: .RegularExpressionSearch  //Let rangeOfString() know that you aren't 
                                                   //literally searching for the string you specified-- 
                                                   //rather it's a regex.
        )
            
        if let _ = match {
            return true
        }

I got the Swift regex syntax here:

benscheirman.com/2014/06/regex-in-swift/

That causes a problem though: backspacing is prevented. A backspace is not a digit. To solve that problem:

[code] //A backspace is a 0 length string:

    if string.characters.count == 0 {
        return true
    }[/code]

There are still other problems, though: What if the current text does not have a decimal point, and the user pastes in text that contains two decimal points? The pasted text should be rejected.


#12

@bubadude,

In turns out that after hitting a backspace, the replacement string doesn’t contain any characters. Here’s the proof:

//In your app: print("Replacement string: -->\(string)<--")

The output after hitting the backspace in the iPhone keypad OR hitting delete on your keyboard is:

But, that isn’t the whole story because there are non printing characters that can produce the same output:

[code]//In a playground:

let string = “\u{7}” //If you look at an ascii chart, 7 is the bell character
print(“Replacement string: -->(string)<–”)

–output:–
Replacement string: -->a<–[/code]

However, check this out:

[code]//In a playground:

let string = "\u{7}"
print(“Replacement string: -->(string)<–”)

print(string.characters.count)

–output:–
Replacement string: --><–
1[/code]
That shows that non-printing characters still show up in a string’s count. When you hit a backspace, what is the result of string.characters.count?

[code]
//In your app:

print(“Replacement string: -->(string)<–”)
print(string.characters.count)

–output:–
Replacement string: --><–
0[/code]
Ahhhhh. So the string you get in your app for the replacement string is truly a blank string.

In your code, the condition on the right side of the || is:

When you hit a backspace, the replacement string is “”, and a blank string does not contain any numbers or decimal points in it, so that condition is true, which means that the whole || condition is true, and then your code returns false, which doesn’t allow the backspace to proceed. (By the way, “decimals” are numbers. A more appropriate name for your variable would be something like numberOrPeriodInString.) Also note, one of the other conditionals on the left of the || already checks for a period in the replacement text, so searching for a period again is a waste of time.


Bronze Challenge - is there a simpler way
#13

[code]let existingTextHasDecimalSeparator = textField.text?.rangeOfString(".")
let replacementTextHasDecimalSeparator = string.rangeOfString(".")
if existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil {
return false
}

    let letterCharacters = NSCharacterSet.letterCharacterSet()
    let containsLetterCharacter = string.rangeOfCharacterFromSet(letterCharacters)
    if containsLetterCharacter != nil {
        return false
    } else {
        return true
    }

[/code]

This removes all letters but preserves the backspace and period (fulfils the wording of the bronze challenge). However, it won’t stop special characters such as “&”. Alternatively you could preserve NSCharacterSet.decimalDigitCharacterSet and “.” but I couldn’t see how to preserve the backspace.


#14

@JasonK,

If you are still interested, read the post prior to yours.

I used a regex for that. I posted an example up there somewhere.

Also something to consider: Are you able to paste in 85.6.7.8?

This exercise is such a thorny problem I’m surprised that the experts haven’t written a long function covering all the possible cases and optimized with the swiftest code, and made it available as a UITextField method, named something like isValidDecimalNumberIfReplacementTextWereAllowedToProceed(). Hmmm…that gives me an idea: why not add the existing string to the replacement string, then check that for 1 decimal point and all digits–instead of checking the pieces separately, which makes the logic more tortured.


#15

Here’s my solution, which took me a while to arrive at, but which seems to work great:
gist.github.com/01392e4da9037c2868d4

However, I didn’t use NSCharacterSet and I’m a little confused about how I was expected to use it. The problem I couldn’t get around is that NSCharacterSet.decimalDigitCharacterSet doesn’t contain a decimal point or negative sign as far as I can tell, which should both be valid input here, so (I think?) any statement using decimalDigitCharacterSet would had to have been something like, “if the character is not in decimalDigitCharacterSet AND it’s not a decimal or negative sign then do stuff”, which seems more complex and verbose than simply saying, “if the character is in this set of valid of characters then do stuff”.

It’s very possible I have missed something or that I am misunderstanding NSCharacterSet. I’d welcome feedback.


#16

Here’s what I did:

let allowedSet = NSCharacterSet(charactersInString: ".1234567890-")
        
let rangeOfNonDecimal = string.rangeOfCharacterFromSet(allowedSet.invertedSet)
        
if (rangeOfNonDecimal != nil){
     return false
}

basically does what you were talking about, and it’s pretty simple. defines the allowed characters, and then checks against the invertedSet (anything not in the allowed set). this doesn’t deal with some of the copy/paste issues talked about above


#17

I have this solution. Dosen’t seem to be so complicated

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
        
        let exsitingTextHasDecimalSeparator = textField.text?.rangeOfString(".")
        let replacementTextHasDecimalSeparator = string.rangeOfString(".")
        let letters = NSCharacterSet.letterCharacterSet()
        let replacmentTextHasAlphabeticCharacters = string.rangeOfCharacterFromSet(letters)
        
        if(replacmentTextHasAlphabeticCharacters != nil) {
            return false
        } else {
            if exsitingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil{
                return false
            } else {
                return true
            }
        }

    }

Ok I see 7stud7stud already posted the same solution…


#18

[code]let rangeOfNonDecimal = string.rangeOfCharacterFromSet(allowedSet.invertedSet)

    if rangeOfNonDecimal != nil {
        return false
    }
    
    if existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil {
        return false
    } else {
        return true
    }

[/code]

Why does rangeOfNonDecimal != nil need to be in its own conditional and not with existingTextHasDecimalSeparator != nil && replacementTextHasDecimalSeparator != nil?


#19

It’s usually best to break up compound conditionals into simpler conditionals if possible. It makes the logic easier to understand. And with Swift, the lines of code get so long it’s a practical matter as well.


#20

my approach was to pretend the change was allowed and work on the candidate change. Essentially a few special cases, allow +/- sign and use the same Double() converter. Works with Copy/Pasted text. If it were a real app I would find a way to avoid multiple leading 0’s but otherwise it seems to handle most of the edge cases people have found with copy/paste, backspace, etc. Not sure how it handles encoded unicode strings. \u007 etc.

func textField(textField: UITextField
    , shouldChangeCharactersInRange range: NSRange
    , replacementString string: String) -> Bool {
    
    print("Current     text: \(textField.text)")
    print("Replacement Text: \(string)")
    
    let current = textField.text ?? ""
    let candidate = (current as NSString).stringByReplacingCharactersInRange(range, withString: string)
    
    // disallow all invalid characters and duplicate/misplaced special characters
    var pluses = 0
    var minuses = 0
    var decimals = 0
    var left = 0
    
    for c in candidate.unicodeScalars
    {
        
        let cc = UTF32Char(c)
        if allowedChars.longCharacterIsMember(cc) { return false }
        
        switch c {
        case "+": pluses = left;  break
        case "-": minuses = left;  break
        case ".": decimals += 1; break
        default:
            break
        }
        
        left += 1
    }
    
    if (pluses > 0) || (minuses > 0) || (decimals > 1) { return false }
    
    // special case valid values
    switch candidate {
    case "+", "-",".", "-.", "+.", "": return true
    default: break
    }
    
    if Double(candidate) == nil {
        return false
    }
            
    return true
    
}