[Résolu] UITextfield : Saisie d'un nombre décimal ou currency

iLandesiLandes Membre
février 2015 modifié dans API UIKit #1

Je suis en train de faire des recherches sur UITextfield et sur le moyen d'obliger l'utilisateur à  saisir un nombre (décimal) ou une somme. Le tout dans l'unité monétaire et le format numérique (séparateur de millier, séparateur décimal etc).


 


Je ne suis pas le seul à  m'être poser la question et j'étudie quelques pods trouvés ici ou là . J'ai aussi explorer les méthode délégué notamment :


 


 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool


 


 


Mes pistes :


Je souhaiterais des conseils ainsi que votre retour d'expérience. Pour l'instant rien ne me plait vraiment : soit trop souple soit trop rigide (donc contraignant pour l'utilisateur)


 


De plus je souhaite connaà®tre la best practice, d'après vous : formater au fur et à  mesure de la saisie. formater après la saisie d'un décimal, ne pas utiliser de uitexfield ?


 


D'avance merci de vos retours


Réponses

  • Joanna CarterJoanna Carter Membre, Modérateur
    février 2015 modifié #2
    Moi, j'utilise les claviers popovers "sur mesure" pour limiter ce que l'on puisse saisir.
  • Merci de ta réponse


     


    Je ne connaissais pas tu fais ça comment ?


     


    Sinon tu gères comment le séparateur décimal, tu obliges le point comme séparateur, tu t'adaptes au préf de l'utilisateur ?


  • AliGatorAliGator Membre, Modérateur
    Un UIViewController custom avec 10 boutons (+ 1 pour le séparateur décimal et un pour le delete), un UIPopOverController, et le tour est joué...
  • iLandesiLandes Membre
    février 2015 modifié #5

    Que ce passe-t-il si l'utilisateur branche un clavier externe (comme c'est possible sur ipad) ?


     


    S'il copie/colle depuis un autre textfield ?


  • iLandesiLandes Membre
    février 2015 modifié #6

    Après mes réflexion j'ai pondu cela. Qui semble fonctionner. Qu'en pensez-vous ?



        func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {  
            let decimalSeparator:Character = Character (currencyFormatter().currencyDecimalSeparator!)
            let digitalCharacters = "0123456789" + String(decimalSeparator)
            
            // returns false if at least one character in string is invalid
            for character in string {
                var characterIsValid = false
                for validCharacter in digitalCharacters {
                    if character == validCharacter {
                        characterIsValid = true
                    }
                    
                }
                if !characterIsValid {
                    return  false
                }
                
            }
            
            
            // returns false if there is more than one decimal separator in potentially new string
            let potentiallyString:String = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
            
            var decimalSeparatorCount = 0
            
            for character in potentiallyString {
                if character == decimalSeparator {
                    decimalSeparatorCount++
                }
            }
            
            if decimalSeparatorCount > 1 {
                return false
            }
            
            
            return true
        }

    Je pense utiliser le type Set disponible dans Swift 1.2


  • Manque la gestion du symbole monétaire. Je le connais :



       let currencySymbol:Character = Character (currencyFormatter().currencySymbol!)

    Mais j'aimerai être sûr qu'il est à  la bonne place. Comment savoir s'il doit être devant ou derrière ?


  • AliGatorAliGator Membre, Modérateur
    Pourquoi ne pas utiliser NSCharacterSet pour faire ce genre de test ?!

    Quitte à  partir de NSMutableCharacterSet.decimalDigitCharacterSet() et appeler dessus addCharactersInString pour y ajouter le currencyDecimalSeparator et currencySymbol pour avoir le set complet.

    Et après tu as des méthodes pour vérifier qu'une String ne contient que des caractères d'un NSCharacterSet donné.

    Après, ce n'est pas aussi simple en pratique : (1) le currencySymbol doit se trouver à  la bonne place (selon la NSLocale courante) et (2) le decimalSeparator ne doit être présent qu'une seule fois.
  • iLandesiLandes Membre
    février 2015 modifié #9


    Pourquoi ne pas utiliser NSCharacterSet pour faire ce genre de test ?!




     


    J'ai regardé NSCharacterSet mais franchement, vu que mon code est en swift et que dans la version 1.2 est prévu un type Set plus sexy. Je modifierais mon code avec un Set à  ce moment là . (J'avais parler de Set sur le forum ici)


  • AliGatorAliGator Membre, Modérateur
    Je connais le type Set de Swift, mais ce n'est pas de ça dont je te parle.
    Set et NSCharacterSet ne sont pas du tout les mêmes classes.

    - Le type Set de Swift 1.2 s'apparente à  NSSet.
    - Le type NSCharacterSet n'a pas d'équivalent en Swift.

    NSCharacterSet offre des possibilités que NSSet / Set n'offrent pas. Comme par exemple :
    - calculer le NSCharacterSet inverse d'un NSCharacterSet donné (pour trouver tous les caractères qui NE SONT PAS dans une liste de caractères)
    - Utiliser les méthodes de String/NSString qui prennent un NSCharacterSet en paramètre, comme rangeOfCharacterFromSet: (et tester s'il est NSNotFound pour s'assurer qu'il n'y a aucun caractère non autorisé) ou stringByTrimmingCharactersInSet (qui peut servir pour enlever tous les caractères non autorisés d'une chaà®ne)
    - Gérer l'Unicode (par exemple si tu autorises des caractères qui peuvent être représentés par plusieurs codepoints UniCode différents " comme c'est le cas de plusieurs caractères, comme les caractères accentués, mais aussi d'autres, pourquoi pas le symbole monétaire ou le point par exemple " alors NSCharacterSet saura gérer le cas et autoriser toutes les variantes.
    etc
  • Petite évolution du code. J'y ai aussi inclu une variable qui manquait dans le code ci-dessous


     


    Où l'on apprend que pour le Yen japonais il n'y a pas de chiffres après la virgule.



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

    var currencyFormatter:NSNumberFormatter {
    get {
    let formatter = NSNumberFormatter()
    formatter.locale = NSLocale.currentLocale()

    // Only for test
    //formatter.locale = NSLocale(localeIdentifier: "fr_FR")
    //formatter.locale = NSLocale(localeIdentifier: "en_US")
    formatter.locale = NSLocale(localeIdentifier: "jp_JP")
    formatter.numberStyle = .CurrencyStyle
    return formatter
    }
    }

    let maximumFractionDigits = currencyFormatter.maximumFractionDigits

    let decimalSeparator:Character = Character (currencyFormatter.currencyDecimalSeparator!)

    let currencySymbol:Character = Character (currencyFormatter.currencySymbol!)

    // SEE ON SWIFT 1.2 : USE Set type ??
    // SEE ON SWIFT 1.2 : USE LET vs VAR ??
    var digitalCharacters = "0123456789" + String (currencySymbol)
    if maximumFractionDigits > 0 {
    digitalCharacters += String(decimalSeparator)
    }

    // returns false if at least one character in string is invalid
    for character in string {
    var characterIsValid = false
    for validCharacter in digitalCharacters {
    if character == validCharacter {
    characterIsValid = true
    }

    }
    if !characterIsValid {
    return false
    }

    }
    let potentiallyString:String = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)

    // returns false if there is more than one decimal separator in potentially new string or
    // to much fraction digits
    var decimalSeparatorCount = 0
    var fractionDigitsCount = -1

    for character in potentiallyString {
    if character == decimalSeparator {
    decimalSeparatorCount++
    }
    if decimalSeparatorCount == 1 {
    fractionDigitsCount++
    }

    }

    if decimalSeparatorCount > 1 || fractionDigitsCount > maximumFractionDigits {
    return false
    }


    // returns false if there is more than one decimal separator in potentially new string
    var currencySymbolCount = 0

    for character in potentiallyString {
    if character == decimalSeparator {
    currencySymbolCount++
    }
    }

    if currencySymbolCount > 1 {
    return false
    }

    // returns false if currencySymbol is not

    return true
    }


    Reste la position du symbole monétaire à  gérer


  • AliGatorAliGator Membre, Modérateur
    Si tu veux complètement migrer vers Swift et sa philosophie tu devrais prendre l'habitude de faire du code + fonctionnel qu'iteratif comme tu fais, genre à  coup de map ou de reduce par exemple. Ou de componentsSeparatedByString à  la limite plutôt que tant de boucles "for".
  • Joanna CarterJoanna Carter Membre, Modérateur

    Voici le code que j'utilise pour déterminer quels boutons à  activer :



    - (void)adjustNumericKeys
    {
    NSString *fieldContents = self.targetTextField.text;

    NSUInteger textLength = fieldContents.length;

    BOOL buttonEnabled = NO;

    NSCharacterSet *invalidNumbers;

    for (UIButton *button in self.numberButtons)
    {
    switch (textLength)
    {
    case 1:
    {
    if ([fieldContents isEqualToString:@"0"])
    {
    invalidNumbers = [NSCharacterSet decimalDigitCharacterSet];

    buttonEnabled = [button.titleLabel.text rangeOfCharacterFromSet:invalidNumbers].location == NSNotFound;
    }
    else
    {
    buttonEnabled = YES;
    }
    }
    break;

    default:
    buttonEnabled = YES;
    break;
    }

    button.enabled = buttonEnabled;
    }
    }

    - (void)adjustDecimalKey
    {
    NSString *targetText = self.targetTextField.text;

    NSUInteger textLength = targetText.length;

    NSLocale *locale = [NSLocale autoupdatingCurrentLocale];

    NSString *decimalSeparator = [locale objectForKey:NSLocaleDecimalSeparator];

    self.decimalSeparatorButton.enabled = textLength > 0 && [targetText rangeOfString:decimalSeparator].location == NSNotFound;
    }

    Je ne accepte pas le currencySymbol en saisissant le texte, je le supprime en entrant dans le textField et je le rajoute en y sortant


  • iLandesiLandes Membre
    février 2015 modifié #14

    Merci Ali,


     


    Je tente de m'améliorer mais il se fait tard, j'ai viré quelques boucles for  xd



    func currencyFormatter() -> NSNumberFormatter {
    let formatter = NSNumberFormatter()
    formatter.locale = NSLocale.currentLocale()

    // Only for test
    formatter.locale = NSLocale(localeIdentifier: "fr_FR")
    //formatter.locale = NSLocale(localeIdentifier: "en_US")
    //formatter.locale = NSLocale(localeIdentifier: "jp_JP")
    formatter.numberStyle = .CurrencyStyle
    return formatter
    }

    func textFieldShouldReturn(textField: UITextField) -> Bool {
    println("textFieldDidEndEditing")
    return true
    }



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



    let maximumFractionDigits = currencyFormatter().maximumFractionDigits
    let decimalSeparator = currencyFormatter().currencyDecimalSeparator
    let currencySymbol = currencyFormatter().currencySymbol

    // SEE ON SWIFT 1.2 : USE Set type ??
    // SEE ON SWIFT 1.2 : USE LET vs VAR ??
    var digitalCharacters = "0123456789" + currencySymbol!
    if maximumFractionDigits > 0 {
    digitalCharacters += decimalSeparator!
    }

    // returns false if at least one character in string is invalid
    for character in string {
    var characterIsValid = false
    for validCharacter in digitalCharacters {
    if character == validCharacter {
    characterIsValid = true
    }

    }
    if !characterIsValid {
    return false
    }

    }
    let potentiallyString:String = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)

    // returns false if there is more than one decimal separator in potentially new string or
    let separateFractionString = potentiallyString.componentsSeparatedByString(decimalSeparator!)
    if separateFractionString.count > 2 {
    return false
    }


    // returns false if there is to much fraction digits
    if separateFractionString.count == 2 {
    let fractionString:String = potentiallyString.componentsSeparatedByString(decimalSeparator!)[1]

    if countElements(fractionString) > maximumFractionDigits {
    return false
    }
    }



    // returns false if there is more than one decimal separator in potentially new string
    let separateCurrencyString = potentiallyString.componentsSeparatedByString(currencySymbol!)
    if separateCurrencyString.count > 2 {
    return false
    }

    // returns false if currencySymbol is not at the right place!
    // ??
    // ??
    return true
    }

    Pour ceux que cela intéresse j'ai trouvé ça : Map, Filter and Reduce in Swift


  • AliGatorAliGator Membre, Modérateur
    février 2015 modifié #15
    Par exemple convertir l'itératif
    for character in string {
    var characterIsValid = false
    for validCharacter in digitalCharacters {
    if character == validCharacter {
    characterIsValid = true
    }

    }
    if !characterIsValid {
    return false
    }

    }
    En fonctionnel avec reduce :
    let stringToTest = "1237623.34"
    let digitalCharacters = Set("0123456789" + currencySymbol!)

    let onlyDigits = Array(stringToTest).reduce(true) { (accum, char) in
    accum && digitalCharacters.contains(char)
    }
    Ou encore une autre solution, plutôt que d'utiliser un Set, utiliser un Range qui va du Character "0" au Character "9" (mais bon avec cette solution tu peux pas rajouter des éléments en dehors du range 0-9 comme le currencySymbol ou autre, donc ça limite l'usage dans ton cas, c'est plutôt juste pour la culture) :
    let stringToTest = "123762334"
    let digitalCharacters = Character("0")...Character("9")

    let onlyDigits = Array(stringToTest).reduce(true) { (accum, char) in
    accum && digitalCharacters.contains(char)
    }
  • iLandesiLandes Membre
    février 2015 modifié #16

    J'en étais là  :



    var allowedCharacters = "0123456789" + currencySymbol!
    if maximumFractionDigits > 0 {
    allowedCharacters += decimalSeparator!
    }

    println("allowed characters : \(allowedCharacters)")
    // returns false if at least one character in string is invalid

    // Create an `NSCharacterSet` set which includes everything *but* the allowedCharacters
    let inverseSet = NSCharacterSet(charactersInString: allowedCharacters).invertedSet

    // At every character in this "inverseSet" contained in the string,
    // split the string up into components which exclude the characters
    // in this inverse set
    let components = string.componentsSeparatedByCharactersInSet(inverseSet)

    // Rejoin these components
    let filtered = join("", components)

    // If the original string is equal to the filtered string, i.e. if no
    // inverse characters were present to be eliminated, the input is valid
    if string != filtered {
    return false
    }

    Mais j'avoue tu va toujours plus loin, toujours plus haut, toujours plus fort ! :D


     


    C'est du 1.2 ton dernier code ?


  • AliGatorAliGator Membre, Modérateur
    Oui je ne code qu'en 1.2 maintenant ; y'a eu tellement de bonnes choses et d'améliorations en Swift 1.2, tant dans le langage que dans le compilateur, que je me pose même pas la question, je vois pas pourquoi je m'en priverais ;)
  • iLandesiLandes Membre
    février 2015 modifié #18

    Je me jette sur la beta des demain.


     


    M'enfin pour moi 



    let onlyDigits = Array(stringToTest).reduce(true) { (accum, char) in
    accum && digitalCharacters.contains(char)
    }

    J'avoue je n'arrive pas trop à  le lire (l'interpréter en langage humain de mon niveau)


  • AliGatorAliGator Membre, Modérateur
    Reduce et Map sont des fonctions qui ne sont pas évidentes à  comprendre la première fois qu'on les voit je te l'accorde (surtout reduce) mais une fois que tu as compris leur fonctionnement c'est assez simple et tu vois vite leur puissance.


    Et puis surtout ce sont des fonctions qui existent dans tous les langages fonctionnels modernes (ruby, Swift,...) et qui sont vite incontournables donc c'est bon de les maà®triser.
  • Globalement je comprends ça  ::)



    let numbers = [1,2,3]
    let squares = numbers.map { $0 * $0 }
    let sum = numbers.reduce(0) { $0 + $1 }

    C'est de mon niveau ^^


     


    Mais alors ça  ;D



    let onlyDigits = Array(stringToTest).reduce(true) { (accum, char) in
    accum && digitalCharacters.contains(char)
    }

  • Bon sinon ça serait pas plus simple de valider le contenu de la chaine avec une expression régulière (NSRegularExpression) ?


     


    Surtout qu'on peut trouver tout un tas d'implémentation en swift (qui encapsulent NSRegularExpression), comme par exemple ici : https://gist.github.com/grosch/d3daeec1eefcf1614442

Connectez-vous ou Inscrivez-vous pour répondre.