Formatieren eines UITextField für Kreditkarteneingaben wie (xxxx xxxx xxxx xxxx)


72

Ich möchte eine UITextFieldfür die Eingabe einer Kreditkartennummer so formatieren, dass nur Ziffern eingegeben werden können und Leerzeichen automatisch eingefügt werden, sodass die Nummer folgendermaßen formatiert wird:

XXXX XXXX XXXX XXXX

Wie kann ich das machen?


1
Wenn Sie Open Source-Bibliotheken verwenden können, würde ich dringend empfehlen, sich PaymentKit ( github.com/stripe/PaymentKit ) anzusehen . Sie haben einen Formatierer, den Sie verwenden können, und er funktioniert für alle Arten von Karten (und hat auch einen Validator für Luhn Check und so weiter).
Mike Welsh

@MikeWelsh faszinierend und vielleicht - soweit ich weiß - ein besserer Ansatz als meine Antwort, aber ich habe nicht die Zeit oder die Neigung, mich damit zu befassen (zumal ich keinen Mac mehr besitze und es nicht getan habe) iOS-Entwicklung in über einem Jahr). Wenn Sie Erfahrung mit der Bibliothek haben, wäre das Schreiben einer Antwort mit einem einfachen Beispiel für deren Verwendung für zukünftige Leser wahrscheinlich viel wertvoller als nur ein Kommentar.
Mark Amery

Diese Antwort kann hilfreich sein, wenn Sie nach einem dynamischen Ansatz suchen. stackoverflow.com/a/38560759/3947151
Tesan3089

Diese Frage zieht weiterhin Antworten von Leuten an, die glauben, dass sie helfen, indem sie eine kürzere, einfachere Antwort als meine (akzeptierte) Antwort liefern. Diese Antworten in der Tat sind kürzer und einfacher - und als Folge davon, nicht ein einziger von ihnen arbeitet! (Und ja , ich habe jeden einzelnen persönlich getestet.) Dies ist ein täuschend schweres Problem, Leute! Wenn Sie versuchen möchten, eine bessere Antwort zu geben, lesen Sie zumindest den Abschnitt "Erklärung" meiner Antwort und die vielen, vielen Kommentare, die ich hinterlassen habe, um zu erklären, wie die Implementierungen anderer Leute fehlerhaft sind, und überprüfen Sie, ob Sie nicht fehlschlagen auf die gleiche Weise.
Mark Amery

Diese Antwort hilft, wenn Sie eine kompakte Lösung und in schneller Sprache stackoverflow.com/questions/37190620/…
Teena nath Paul

Antworten:


144

Wenn Sie Swift verwenden, lesen Sie meinen Port dieser Antwort für Swift 4 und verwenden Sie diesen stattdessen.

Wenn Sie in Objective-C sind ...

Fügen Sie zunächst Ihre UITextFieldDelegateInstanzvariablen hinzu ...

NSString *previousTextFieldContent;
UITextRange *previousSelection;

... und diese Methoden:

// Version 1.3
// Source and explanation: http://stackoverflow.com/a/19161529/1709587
-(void)reformatAsCardNumber:(UITextField *)textField
{
    // In order to make the cursor end up positioned correctly, we need to
    // explicitly reposition it after we inject spaces into the text.
    // targetCursorPosition keeps track of where the cursor needs to end up as
    // we modify the string, and at the end we set the cursor position to it.
    NSUInteger targetCursorPosition = 
        [textField offsetFromPosition:textField.beginningOfDocument
                           toPosition:textField.selectedTextRange.start];

    NSString *cardNumberWithoutSpaces = 
        [self removeNonDigits:textField.text
                  andPreserveCursorPosition:&targetCursorPosition];

    if ([cardNumberWithoutSpaces length] > 19) {
        // If the user is trying to enter more than 19 digits, we prevent 
        // their change, leaving the text field in  its previous state.
        // While 16 digits is usual, credit card numbers have a hard 
        // maximum of 19 digits defined by ISO standard 7812-1 in section
        // 3.8 and elsewhere. Applying this hard maximum here rather than
        // a maximum of 16 ensures that users with unusual card numbers
        // will still be able to enter their card number even if the
        // resultant formatting is odd.
        [textField setText:previousTextFieldContent];
        textField.selectedTextRange = previousSelection;
        return;
    }

    NSString *cardNumberWithSpaces = 
        [self insertCreditCardSpaces:cardNumberWithoutSpaces
           andPreserveCursorPosition:&targetCursorPosition];

    textField.text = cardNumberWithSpaces;
    UITextPosition *targetPosition = 
        [textField positionFromPosition:[textField beginningOfDocument]
                                 offset:targetCursorPosition];

    [textField setSelectedTextRange:
        [textField textRangeFromPosition:targetPosition
                              toPosition:targetPosition]
    ];
}

-(BOOL)textField:(UITextField *)textField 
         shouldChangeCharactersInRange:(NSRange)range 
                     replacementString:(NSString *)string
{
    // Note textField's current state before performing the change, in case
    // reformatTextField wants to revert it
    previousTextFieldContent = textField.text;
    previousSelection = textField.selectedTextRange;

    return YES;
}

/*
 Removes non-digits from the string, decrementing `cursorPosition` as
 appropriate so that, for instance, if we pass in `@"1111 1123 1111"`
 and a cursor position of `8`, the cursor position will be changed to
 `7` (keeping it between the '2' and the '3' after the spaces are removed).
 */
- (NSString *)removeNonDigits:(NSString *)string
                andPreserveCursorPosition:(NSUInteger *)cursorPosition 
{
    NSUInteger originalCursorPosition = *cursorPosition;
    NSMutableString *digitsOnlyString = [NSMutableString new];
    for (NSUInteger i=0; i<[string length]; i++) {
        unichar characterToAdd = [string characterAtIndex:i];
        if (isdigit(characterToAdd)) {
            NSString *stringToAdd = 
                [NSString stringWithCharacters:&characterToAdd
                                        length:1];

            [digitsOnlyString appendString:stringToAdd];
        }
        else {
            if (i < originalCursorPosition) {
                (*cursorPosition)--;
            }
        }
    }

    return digitsOnlyString;
}

/*
 Detects the card number format from the prefix, then inserts spaces into
 the string to format it as a credit card number, incrementing `cursorPosition`
 as appropriate so that, for instance, if we pass in `@"111111231111"` and a
 cursor position of `7`, the cursor position will be changed to `8` (keeping
 it between the '2' and the '3' after the spaces are added).
 */
- (NSString *)insertCreditCardSpaces:(NSString *)string
                          andPreserveCursorPosition:(NSUInteger *)cursorPosition
{
    // Mapping of card prefix to pattern is taken from
    // https://baymard.com/checkout-usability/credit-card-patterns

    // UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
    bool is456 = [string hasPrefix: @"1"];

    // These prefixes reliably indicate either a 4-6-5 or 4-6-4 card. We treat all
    // these as 4-6-5-4 to err on the side of always letting the user type more
    // digits.
    bool is465 = [string hasPrefix: @"34"] ||
                 [string hasPrefix: @"37"] ||

                 // Diners Club
                 [string hasPrefix: @"300"] ||
                 [string hasPrefix: @"301"] ||
                 [string hasPrefix: @"302"] ||
                 [string hasPrefix: @"303"] ||
                 [string hasPrefix: @"304"] ||
                 [string hasPrefix: @"305"] ||
                 [string hasPrefix: @"309"] ||
                 [string hasPrefix: @"36"] ||
                 [string hasPrefix: @"38"] ||
                 [string hasPrefix: @"39"];

    // In all other cases, assume 4-4-4-4-3.
    // This won't always be correct; for instance, Maestro has 4-4-5 cards
    // according to https://baymard.com/checkout-usability/credit-card-patterns,
    // but I don't know what prefixes identify particular formats.
    bool is4444 = !(is456 || is465);

    NSMutableString *stringWithAddedSpaces = [NSMutableString new];
    NSUInteger cursorPositionInSpacelessString = *cursorPosition;
    for (NSUInteger i=0; i<[string length]; i++) {
        bool needs465Spacing = (is465 && (i == 4 || i == 10 || i == 15));
        bool needs456Spacing = (is456 && (i == 4 || i == 9 || i == 15));
        bool needs4444Spacing = (is4444 && i > 0 && (i % 4) == 0);

        if (needs465Spacing || needs456Spacing || needs4444Spacing) {
            [stringWithAddedSpaces appendString:@" "];
            if (i < cursorPositionInSpacelessString) {
                (*cursorPosition)++;
            }
        }
        unichar characterToAdd = [string characterAtIndex:i];
        NSString *stringToAdd =
        [NSString stringWithCharacters:&characterToAdd length:1];

        [stringWithAddedSpaces appendString:stringToAdd];
    }

    return stringWithAddedSpaces;
}

Zweitens festlegen reformatCardNumber:, dass aufgerufen wird, wenn das Textfeld ein UIControlEventEditingChangedEreignis auslöst:

[yourTextField addTarget:yourTextFieldDelegate 
                             action:@selector(reformatAsCardNumber:)
                   forControlEvents:UIControlEventEditingChanged];

(Natürlich müssen Sie dies irgendwann tun, nachdem Ihr Textfeld und sein Delegat instanziiert wurden. Wenn Sie Storyboards verwenden, ist die viewDidLoadMethode Ihres View Controllers ein geeigneter Ort.

Einige Erklärungen

Dies ist ein täuschend kompliziertes Problem. Drei wichtige Punkte, die möglicherweise nicht sofort offensichtlich sind (und deren vorherige Antworten hier alle nicht berücksichtigt werden):

  1. Während das XXXX XXXX XXXX XXXXFormat für Kredit- und Debitkartennummern das gebräuchlichste ist, ist es nicht das einzige. Zum Beispiel haben American Express-Karten 15-stellige Zahlen, die normalerweise XXXX XXXXXX XXXXXwie folgt geschrieben sind:

    Eine American Express Karte

    Sogar Visa-Karten können weniger als 16 Ziffern haben, und Maestro-Karten können mehr haben:

    Eine russische Maestro-Karte mit 18 Ziffern

  2. Es gibt mehr Möglichkeiten für den Benutzer, mit einem Textfeld zu interagieren, als nur einzelne Zeichen am Ende der vorhandenen Eingabe einzugeben. Sie haben auch die Benutzer richtig zu behandeln Hinzufügen Zeichen in der Mitte der Zeichenfolge, das Löschen einzelner Zeichen, mehrere ausgewählte Zeichen Löschen und Einfügen in mehreren Zeichen. Einige einfachere / naivere Ansätze für dieses Problem werden einige dieser Interaktionen nicht richtig handhaben. Der perverseste Fall ist, dass ein Benutzer mehrere Zeichen in die Mitte der Zeichenfolge einfügt, um andere Zeichen zu ersetzen, und diese Lösung ist allgemein genug, um dies zu handhaben.

  3. Sie müssen den Text des Textfelds nicht nur ordnungsgemäß neu formatieren, nachdem der Benutzer ihn geändert hat, sondern auch den Textcursor sinnvoll positionieren . Naive Herangehensweisen an das Problem, die dies nicht berücksichtigen, führen in einigen Fällen mit ziemlicher Sicherheit zu Dummheiten mit dem Textcursor (z. B. wenn Sie ihn an das Ende des Textfelds setzen, nachdem der Benutzer eine Ziffer in der Mitte hinzugefügt hat ).

Um das Problem Nr. 1 zu lösen, verwenden wir die teilweise Zuordnung von Kartennummernpräfixen zu Formaten, die vom Baymard Institute unter https://baymard.com/checkout-usability/credit-card-patterns kuratiert wurden . Wir können den Kartenanbieter automatisch anhand der ersten Ziffern erkennen und (in einigen Fällen) auf das Format schließen und unsere Formatierung entsprechend anpassen. Vielen Dank an cnotethegr8, der diese Idee zu dieser Antwort beigetragen hat.

Die einfachste und einfachste Möglichkeit, mit Problem Nr. 2 (und der im obigen Code verwendeten Methode) umzugehen, besteht darin, alle Leerzeichen zu entfernen und sie jedes Mal an den richtigen Positionen wieder einzufügen, wenn sich der Inhalt des Textfelds ändert, sodass wir keine Zahlen mehr benötigen herausfinden, welche Art von Textmanipulation (Einfügen, Löschen oder Ersetzen) stattfindet, und die Möglichkeiten unterschiedlich behandeln.

Um das Problem Nr. 3 zu lösen, verfolgen wir, wie sich der gewünschte Index des Cursors ändert, wenn wir nicht-stellige Zeichen entfernen und dann Leerzeichen einfügen. Aus diesem Grund führt der Code diese Manipulationen ziemlich ausführlich zeichenweise durch NSMutableString, anstatt die NSStringMethoden zum Ersetzen von Zeichenfolgen zu verwenden.

Schließlich lauert noch eine weitere Falle: Wenn Sie NOvon textField: shouldChangeCharactersInRange: replacementStringUnterbrechungen zurückkehren , wird die Schaltfläche "Ausschneiden" angezeigt, die der Benutzer erhält, wenn er Text im Textfeld auswählt. Deshalb mache ich das nicht. Die Rückkehr NOvon dieser Methode führt dazu, dass 'Ausschneiden' die Zwischenablage einfach überhaupt nicht aktualisiert, und ich kenne keine Korrektur oder Problemumgehung. Infolgedessen müssen wir das Textfeld in einem UIControlEventEditingChangedHandler neu formatieren, anstatt (offensichtlicher) in sich shouldChangeCharactersInRange:selbst.

Glücklicherweise scheinen die UIControl-Ereignishandler aufgerufen zu werden, bevor UI-Updates auf den Bildschirm übertragen werden. Daher funktioniert dieser Ansatz einwandfrei.

Es gibt auch eine ganze Reihe kleinerer Fragen zum genauen Verhalten des Textfelds, auf die offensichtlich keine richtigen Antworten vorliegen:

  • Wenn der Benutzer versucht, etwas einzufügen, das dazu führen würde, dass der Inhalt des Textfelds 19 Stellen überschreitet, sollte der Anfang der eingefügten Zeichenfolge eingefügt werden (bis 19 Stellen erreicht sind) und der Rest abgeschnitten oder überhaupt nichts eingefügt werden ?
  • Wenn der Benutzer versucht, ein einzelnes Leerzeichen zu löschen, indem er seinen Cursor danach positioniert und die Rücktaste drückt, sollte nichts passieren und der Cursor dort bleiben, wo er sich befindet, sollte sich der Cursor um ein Zeichen nach links bewegen (vor dem Leerzeichen platzieren) oder sollte der Ziffer links vom Leerzeichen gelöscht werden, als ob der Cursor bereits links vom Leerzeichen wäre?
  • Wenn der Benutzer die vierte, achte oder zwölfte Ziffer eingibt, sollte sofort ein Leerzeichen eingefügt und der Cursor danach bewegt werden, oder sollte das Leerzeichen erst eingefügt werden, nachdem der Benutzer die fünfte, neunte oder dreizehnte Ziffer eingegeben hat?
  • Wenn der Benutzer die erste Ziffer nach einem Leerzeichen löscht und dies nicht dazu führt, dass das Leerzeichen vollständig entfernt wird, sollte dies dazu führen, dass der Cursor vor oder nach dem Leerzeichen positioniert wird?

Wahrscheinlich ist jede Antwort auf eine dieser Fragen angemessen, aber ich liste sie auf, um zu verdeutlichen, dass es tatsächlich viele Sonderfälle gibt, über die Sie hier sorgfältig nachdenken sollten, wenn Sie besessen genug wären. Im obigen Code habe ich Antworten auf diese Fragen ausgewählt, die mir vernünftig erschienen. Wenn Sie starke Gefühle in Bezug auf einen dieser Punkte haben, die nicht mit dem Verhalten meines Codes kompatibel sind, sollte es einfach genug sein, ihn an Ihre Bedürfnisse anzupassen.


1
Ich bekomme unrecognized selector sent to instanceund ein Thread-Problem, wenn ich das mache. Ideen?
Jordan Feldstein

Ich konnte es mit diesem Diff beheben: cl.ly/image/45182G0Z3r1O Das Speichern der Referenz auf dem Controller scheint zu verhindern, dass Müll gesammelt wird, was den Fehler überhaupt erst verursacht hat. Hoffentlich hilft das! cc @MarkAmery für den Fall, dass er seine Lösung überprüfen und aktualisieren möchte.
Jordan Feldstein

@ JordanFeldsteint froh, dass Sie Ihr Problem gelöst haben. Was Sie beschreiben (Verweise auf Objekte behalten müssen, um zu verhindern, dass sie durch Müll gesammelt werden), ist ein Standardproblem in Objective-C mit ARC und geht meiner Meinung nach über den Rahmen dieser Antwort hinaus. Übrigens ist das Ärgernis, eine solche Buchhaltung durchführen zu müssen, einer der Gründe, warum viele Leute (einschließlich mir und einschließlich Apple-Entwickler in Demos) ihre View-Controller einfach als Delegierten von allem verwenden möchten, anstatt zusätzliche Objekte zu erstellen, als die sie verwendet werden sollen delegieren. Die Verwendung von Pragma-Markierungen in Xcode macht dies auch für komplexe Ansichten leicht handhabbar.
Mark Amery

@MarkAmery Ich habe Ihre Ideen in Form einer kleinen Bibliothek implementiert Textfeld Formatierung zu verwalten github.com/chebur/CHRTextFieldFormatter
chebur

@MarkAmery Die Art und Weise, wie Sie Ihre targetCursorPositionam Anfang berechnen, ist kaputt. Versuchen Sie, ein Zeichen hinzuzufügen, das kein Unicode-Skalar ist, wie z. B. ein Emoji. Die Cursorposition ist ungenau.
HHK

31

Unten finden Sie eine funktionierende Swift 4-Portierung der Antwort von Logicopolis (die wiederum eine Swift 2-Portierung einer alten Version meiner in Objective-C akzeptierten Antwort ist ), die mit dem Trick von cnotethegr8 zur Unterstützung von Amex-Karten erweitert und anschließend zur Unterstützung weiterer Karten erweitert wurde Formate. Ich schlage vor, die akzeptierte Antwort zu überprüfen, falls Sie dies noch nicht getan haben, da dies die Motivation für einen Großteil dieses Codes erklärt.

Beachten Sie, dass die minimale Reihe von Schritten erforderlich ist, um dies in Aktion zu sehen:

  1. Erstellen Sie eine neue Single View App in Swift.
  2. Auf Main.storyboardein hinzufügen Textfeld .
  3. Machen Sie den ViewControllerDelegaten des Textfeldes .
  4. Fügen Sie den folgenden Code in ein ViewController.swift.
  5. Verbinden Sie das IBOutletmit dem Textfeld .
  6. Führen Sie Ihre App aus und geben Sie das Textfeld ein .

import UIKit

class ViewController: UIViewController, UITextFieldDelegate {
    private var previousTextFieldContent: String?
    private var previousSelection: UITextRange?
    @IBOutlet var yourTextField: UITextField!;

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib
        yourTextField.addTarget(self, action: #selector(reformatAsCardNumber), for: .editingChanged)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        previousTextFieldContent = textField.text;
        previousSelection = textField.selectedTextRange;
        return true
    }

    @objc func reformatAsCardNumber(textField: UITextField) {
        var targetCursorPosition = 0
        if let startPosition = textField.selectedTextRange?.start {
            targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition)
        }

        var cardNumberWithoutSpaces = ""
        if let text = textField.text {
            cardNumberWithoutSpaces = self.removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
        }

        if cardNumberWithoutSpaces.count > 19 {
            textField.text = previousTextFieldContent
            textField.selectedTextRange = previousSelection
            return
        }

        let cardNumberWithSpaces = self.insertCreditCardSpaces(cardNumberWithoutSpaces, preserveCursorPosition: &targetCursorPosition)
        textField.text = cardNumberWithSpaces

        if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
            textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
        }
    }

    func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
        var digitsOnlyString = ""
        let originalCursorPosition = cursorPosition

        for i in Swift.stride(from: 0, to: string.count, by: 1) {
            let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
            if characterToAdd >= "0" && characterToAdd <= "9" {
                digitsOnlyString.append(characterToAdd)
            }
            else if i < originalCursorPosition {
                cursorPosition -= 1
            }
        }

        return digitsOnlyString
    }

    func insertCreditCardSpaces(_ string: String, preserveCursorPosition cursorPosition: inout Int) -> String {
        // Mapping of card prefix to pattern is taken from
        // https://baymard.com/checkout-usability/credit-card-patterns

        // UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
        let is456 = string.hasPrefix("1")

        // These prefixes reliably indicate either a 4-6-5 or 4-6-4 card. We treat all these
        // as 4-6-5-4 to err on the side of always letting the user type more digits.
        let is465 = [
            // Amex
            "34", "37",

            // Diners Club
            "300", "301", "302", "303", "304", "305", "309", "36", "38", "39"
        ].contains { string.hasPrefix($0) }

        // In all other cases, assume 4-4-4-4-3.
        // This won't always be correct; for instance, Maestro has 4-4-5 cards according
        // to https://baymard.com/checkout-usability/credit-card-patterns, but I don't
        // know what prefixes identify particular formats.
        let is4444 = !(is456 || is465)

        var stringWithAddedSpaces = ""
        let cursorPositionInSpacelessString = cursorPosition

        for i in 0..<string.count {
            let needs465Spacing = (is465 && (i == 4 || i == 10 || i == 15))
            let needs456Spacing = (is456 && (i == 4 || i == 9 || i == 15))
            let needs4444Spacing = (is4444 && i > 0 && (i % 4) == 0)

            if needs465Spacing || needs456Spacing || needs4444Spacing {
                stringWithAddedSpaces.append(" ")

                if i < cursorPositionInSpacelessString {
                    cursorPosition += 1
                }
            }

            let characterToAdd = string[string.index(string.startIndex, offsetBy:i)]
            stringWithAddedSpaces.append(characterToAdd)
        }

        return stringWithAddedSpaces
    }
}

Die Anpassung an andere Situationen - beispielsweise wenn Ihr Delegierter kein Delegierter ist ViewController- bleibt dem Leser als Übung überlassen.


textField.selectedTextRange = textField.textRange (von: targetPosition bis: targetPosition) funktioniert nur dann korrekt, wenn ich es mit DispatchQueue.main.async im Hauptthread versende. Ich nehme an, die Textansicht befindet sich an diesem Punkt in einem Zustand, in dem textField.selectedTextRange wird ignoriert. Das erneute Versenden umgeht das Problem.
Scrrr

Ich denke das kann abstürzen. Geben Sie 4111111111111111111 (sollte wie 4111 1111 1111 1111 111 aussehen) ein, drücken Sie die Rücktaste über die letzten drei Ziffern, schütteln Sie, um rückgängig zu machen, und wählen Sie Rückgängig. *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSBigMutableString substringWithRange:]: Range {20, 0} out of bounds; string length 19'
Josh Paradroid

@ JoshParadroid Oooh, yikes. :( Ich kann Ihnen sofort glauben - ich weiß , dass ich nicht „Undo“ hat die bei der erste in Objective C zurück im Jahr 2013 die Umsetzung Heck, ich bin sicher nicht , ob es auch war „Undo“ -Funktion zurück in iOS 6. I Ich werde einen Blick darauf werfen, wenn ich die Gelegenheit dazu bekomme und sehen, ob ich irgendetwas tun kann, um das Problem zu beheben, aber ich weiß nicht, ob es überhaupt eine Möglichkeit gibt, die Grenze zwischen dem zu überschreiten, was zur ordnungsgemäßen Unterstützung von "Rückgängig" und erforderlich ist Erfüllen Sie auch die anderen Einschränkungen, die ich in der akzeptierten Antwort skizziere. Vielleicht ist es möglich, die Schaltfläche zu unterdrücken, damit der Benutzer sie überhaupt nicht "rückgängig machen" kann?
Mark Amery

@MarkAmery für die Situation, in der ich dies derzeit verwende, wäre es gut genug, es zu unterdrücken, aber ich habe derzeit nicht viel Glück dabei. Vielen Dank für die schnelle Antwort auf eine alte Frage.
Josh Paradroid

1
@MarkAmery textField.undoManager?.removeAllActions()am Ende von reformatAsCardNumberstoppt den Absturz. Es ist keine gute Lösung, aber es funktioniert.
Josh Paradroid

26

Sie können wahrscheinlich meinen Code optimieren oder es gibt einen einfacheren Weg, aber dieser Code sollte funktionieren:

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {

    __block NSString *text = [textField text];

    NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789\b"];
    string = [string stringByReplacingOccurrencesOfString:@" " withString:@""];
    if ([string rangeOfCharacterFromSet:[characterSet invertedSet]].location != NSNotFound) {
        return NO;
    }

    text = [text stringByReplacingCharactersInRange:range withString:string];
    text = [text stringByReplacingOccurrencesOfString:@" " withString:@""];

    NSString *newString = @"";
    while (text.length > 0) {
        NSString *subString = [text substringToIndex:MIN(text.length, 4)];
        newString = [newString stringByAppendingString:subString];
        if (subString.length == 4) {
            newString = [newString stringByAppendingString:@" "];
        }
        text = [text substringFromIndex:MIN(text.length, 4)];
    }

    newString = [newString stringByTrimmingCharactersInSet:[characterSet invertedSet]];

    if (newString.length >= 20) {
        return NO;
    }

    [textField setText:newString];

    return NO;
}

1
Bei dieser Lösung gibt es mindestens einen signifikanten Fehler: Wenn ich beispielsweise '1234' eingebe und dann den Textcursor auf kurz nach der '1' bewege und dann ein Zeichen eingebe oder lösche, springt mein Textcursor plötzlich zum Ende des Textfeldes wieder.
Mark Amery

Würde dies den gespeicherten Text zerstören? oder wirkt es sich nur auf den angezeigten Text aus?
James Campbell

12

Ich finde das gut:

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
    {

        NSLog(@"%@",NSStringFromRange(range));

        // Only the 16 digits + 3 spaces
        if (range.location == 19) {
            return NO;
        }

        // Backspace
        if ([string length] == 0)
            return YES;

        if ((range.location == 4) || (range.location == 9) || (range.location == 14))
        {

            NSString *str    = [NSString stringWithFormat:@"%@ ",textField.text];
            textField.text   = str;
        }

        return YES;
    }

1
Diese Antwort ist auch gebrochen. Die Positionierung der Leerzeichen wird unterbrochen, wenn ich nach Eingabe der Nummer zurück gehe und Zeichen lösche.
Mark Amery

1
bricht es damit? if ([string length] == 0) return YES;
Lucas

ja - hier sind noch viele kaputt. Zum einen kann ich so lange eine Zahl eingeben, wie ich möchte, solange ich meinen Textcursor links neben dem Feld bewege!
Mark Amery

11

Swift 3- Lösung mit Fawkes- Antwort als Basis. Unterstützung für das Amex-Kartenformat hinzugefügt. Reformation hinzugefügt, wenn der Kartentyp geändert wurde.

Erstelle zuerst eine neue Klasse mit diesem Code:

extension String {

    func containsOnlyDigits() -> Bool
    {

        let notDigits = NSCharacterSet.decimalDigits.inverted

        if rangeOfCharacter(from: notDigits, options: String.CompareOptions.literal, range: nil) == nil
        {
            return true
        }

        return false
    }
}
import UIKit

var creditCardFormatter : CreditCardFormatter
{
    return CreditCardFormatter.sharedInstance
}

class CreditCardFormatter : NSObject
{
    static let sharedInstance : CreditCardFormatter = CreditCardFormatter()

    func formatToCreditCardNumber(isAmex: Bool, textField : UITextField, withPreviousTextContent previousTextContent : String?, andPreviousCursorPosition previousCursorSelection : UITextRange?) {
        var selectedRangeStart = textField.endOfDocument
        if textField.selectedTextRange?.start != nil {
            selectedRangeStart = (textField.selectedTextRange?.start)!
        }
        if  let textFieldText = textField.text
        {
            var targetCursorPosition : UInt = UInt(textField.offset(from:textField.beginningOfDocument, to: selectedRangeStart))
            let cardNumberWithoutSpaces : String = removeNonDigitsFromString(string: textFieldText, andPreserveCursorPosition: &targetCursorPosition)
            if cardNumberWithoutSpaces.characters.count > 19
            {
                textField.text = previousTextContent
                textField.selectedTextRange = previousCursorSelection
                return
            }
            var cardNumberWithSpaces = ""
            if isAmex {
                cardNumberWithSpaces = insertSpacesInAmexFormat(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
            }
            else
            {
                cardNumberWithSpaces = insertSpacesIntoEvery4DigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
            }
            textField.text = cardNumberWithSpaces
            if let finalCursorPosition = textField.position(from:textField.beginningOfDocument, offset: Int(targetCursorPosition))
            {
                textField.selectedTextRange = textField.textRange(from: finalCursorPosition, to: finalCursorPosition)
            }
        }
    }

    func removeNonDigitsFromString(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
        var digitsOnlyString : String = ""
        for index in stride(from: 0, to: string.characters.count, by: 1)
        {
            let charToAdd : Character = Array(string.characters)[index]
            if isDigit(character: charToAdd)
            {
                digitsOnlyString.append(charToAdd)
            }
            else
            {
                if index < Int(cursorPosition)
                {
                    cursorPosition -= 1
                }
            }
        }
        return digitsOnlyString
    }

    private func isDigit(character : Character) -> Bool
    {
        return "\(character)".containsOnlyDigits()
    }

    func insertSpacesInAmexFormat(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
        var stringWithAddedSpaces : String = ""
        for index in stride(from: 0, to: string.characters.count, by: 1)
        {
            if index == 4
            {
                stringWithAddedSpaces += " "
                if index < Int(cursorPosition)
                {
                    cursorPosition += 1
                }
            }
            if index == 10 {
                stringWithAddedSpaces += " "
                if index < Int(cursorPosition)
                {
                    cursorPosition += 1
                }
            }
            if index < 15 {
               let characterToAdd : Character = Array(string.characters)[index]
                stringWithAddedSpaces.append(characterToAdd)
            }
        }
        return stringWithAddedSpaces
    }


    func insertSpacesIntoEvery4DigitsIntoString(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
        var stringWithAddedSpaces : String = ""
        for index in stride(from: 0, to: string.characters.count, by: 1)
        {
            if index != 0 && index % 4 == 0 && index < 16
            {
                stringWithAddedSpaces += " "

                if index < Int(cursorPosition)
                {
                    cursorPosition += 1
                }
            }
            if index < 16 {
                let characterToAdd : Character = Array(string.characters)[index]
                stringWithAddedSpaces.append(characterToAdd)
            }
        }
        return stringWithAddedSpaces
    }

}

Fügen Sie in Ihrer ViewControllerClass diese Funktion hinzu

func reformatAsCardNumber(textField:UITextField){
  let formatter = CreditCardFormatter()
  var isAmex = false
  if selectedCardType == "AMEX" {
    isAmex = true
    }
  formatter.formatToCreditCardNumber(isAmex: isAmex, textField: textField, withPreviousTextContent: textField.text, andPreviousCursorPosition: textField.selectedTextRange)
}

Fügen Sie dann Ihrem textField ein Ziel hinzu

youtTextField.addTarget(self, action: #selector(self.reformatAsCardNumber(textField:)), for: UIControlEvents.editingChanged)

Registrieren Sie eine neue Variable und senden Sie den Kartentyp an diese

var selectedCardType: String? {
  didSet{
    reformatAsCardNumber(textField: yourTextField)
  }
}

Danke Fawkes für seinen Code!


1
withPreviousTextContent hat keinen korrekten Wert.
Alexander Belyavskiy

-1; Dies behandelt das Löschen der Ziffer nach einem Leerzeichen nicht richtig. Wenn ich 1234 5678 9012in meinem Textfeld habe und meinen Textcursor nach dem 9und die Rücktaste positioniere , 9wird der gelöscht, aber mein Textcursor endet nach dem 0und nicht nach dem 8.
Mark Amery

8
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
    {
        if textField == CardNumTxt
        {
            let replacementStringIsLegal = string.rangeOfCharacterFromSet(NSCharacterSet(charactersInString: "0123456789").invertedSet) == nil

            if !replacementStringIsLegal
            {
                return false
            }

            let newString = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
            let components = newString.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "0123456789").invertedSet)

            let decimalString = components.joinWithSeparator("") as NSString
            let length = decimalString.length
            let hasLeadingOne = length > 0 && decimalString.characterAtIndex(0) == (1 as unichar)

            if length == 0 || (length > 16 && !hasLeadingOne) || length > 19
            {
                let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int

                return (newLength > 16) ? false : true
            }
            var index = 0 as Int
            let formattedString = NSMutableString()

            if hasLeadingOne
            {
                formattedString.appendString("1 ")
                index += 1
            }
            if length - index > 4
            {
                let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
                formattedString.appendFormat("%@-", prefix)
                index += 4
            }

            if length - index > 4
            {
                let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
                formattedString.appendFormat("%@-", prefix)
                index += 4
            }
            if length - index > 4
            {
                let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
                formattedString.appendFormat("%@-", prefix)
                index += 4
            }


            let remainder = decimalString.substringFromIndex(index)
            formattedString.appendString(remainder)
            textField.text = formattedString as String
            return false
        }
        else
        {
            return true
        }
    }

formattedString.appendFormat ("% @ -", Präfix) chage von "-" jeder anderen Ihrer Wahl


-1; Wie viele andere Antworten hier verhält sich dies schrecklich, wenn Sie den Textcursor bewegen. Bei jeder von mir eingegebenen Ziffer springt der Cursor zur rechten Seite des Textfelds, unabhängig davon, wo ich die Ziffer eingegeben habe.
Mark Amery

Es funktioniert für mich, hat es aber in den neuesten schnellen Code konvertiert.
Shanu Singh

8

Also wollte ich das mit weniger Code, also habe ich den Code hier verwendet und ihn ein wenig neu verwendet. Ich hatte zwei Felder auf dem Bildschirm, eines für die Nummer und eines für das Ablaufdatum, also habe ich es wiederverwendbarer gemacht.

Swift 3 alternative Antwort

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    guard let currentText = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) else { return true }

    if textField == cardNumberTextField {
        textField.text = currentText.grouping(every: 4, with: " ")
        return false
    }
    else { // Expiry Date Text Field
        textField.text = currentText.grouping(every: 2, with: "/")
        return false
    }
}

extension String {
    func grouping(every groupSize: String.IndexDistance, with separator: Character) -> String {
       let cleanedUpCopy = replacingOccurrences(of: String(separator), with: "")
       return String(cleanedUpCopy.characters.enumerated().map() {
            $0.offset % groupSize == 0 ? [separator, $0.element] : [$0.element]
       }.joined().dropFirst())
    }
}

-1. Jeder möchte dies mit weniger Code tun, aber bisher ist es niemandem gelungen. Diese Antwort ist (wie die meisten anderen) auf eine Weise unterbrochen, vor der ich bereits in meiner Antwort gewarnt habe: Wenn Sie den Textcursor an eine andere Stelle als das Ende des Textfelds bewegen und eine Ziffer eingeben, springt der Textcursor auf die Ende des Textfeldes, was nicht passieren sollte.
Mark Amery

5

Noch eine Version der akzeptierten Antwort in Swift 2 ...

Stellen Sie sicher, dass Sie diese in Ihrer Delegateninstanz haben:

private var previousTextFieldContent: String?
private var previousSelection: UITextRange?

Stellen Sie außerdem sicher, dass Ihr Textfeld reformatAsCardNumber aufruft:

textField.addTarget(self, action: #selector(reformatAsCardNumber(_:)), forControlEvents: .EditingChanged)

Ihr Textfelddelegierter muss dies tun:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    previousTextFieldContent = textField.text;
    previousSelection = textField.selectedTextRange;
    return true
}

Zuletzt schließen Sie die folgenden Methoden ein:

func reformatAsCardNumber(textField: UITextField) {
    var targetCursorPosition = 0
    if let startPosition = textField.selectedTextRange?.start {
        targetCursorPosition = textField.offsetFromPosition(textField.beginningOfDocument, toPosition: startPosition)
    }

    var cardNumberWithoutSpaces = ""
    if let text = textField.text {
        cardNumberWithoutSpaces = self.removeNonDigits(text, andPreserveCursorPosition: &targetCursorPosition)
    }

    if cardNumberWithoutSpaces.characters.count > 19 {
        textField.text = previousTextFieldContent
        textField.selectedTextRange = previousSelection
        return
    }

    let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
    textField.text = cardNumberWithSpaces

    if let targetPosition = textField.positionFromPosition(textField.beginningOfDocument, offset: targetCursorPosition) {
        textField.selectedTextRange = textField.textRangeFromPosition(targetPosition, toPosition: targetPosition)
    }
}

func removeNonDigits(string: String, inout andPreserveCursorPosition cursorPosition: Int) -> String {
    var digitsOnlyString = ""
    let originalCursorPosition = cursorPosition

    for i in 0.stride(to: string.characters.count, by: 1) {
        let characterToAdd = string[string.startIndex.advancedBy(i)]
        if characterToAdd >= "0" && characterToAdd <= "9" {
            digitsOnlyString.append(characterToAdd)
        }
        else if i < originalCursorPosition {
            cursorPosition -= 1
        }
    }

    return digitsOnlyString
}

func insertSpacesEveryFourDigitsIntoString(string: String, inout andPreserveCursorPosition cursorPosition: Int) -> String {
    var stringWithAddedSpaces = ""
    let cursorPositionInSpacelessString = cursorPosition

    for i in 0.stride(to: string.characters.count, by: 1) {
        if i > 0 && (i % 4) == 0 {
            stringWithAddedSpaces.appendContentsOf(" ")
            if i < cursorPositionInSpacelessString {
                cursorPosition += 1
            }
        }
        let characterToAdd = string[string.startIndex.advancedBy(i)]
        stringWithAddedSpaces.append(characterToAdd)
    }

    return stringWithAddedSpaces
}

2
Großartige Arbeit - dies ist die einzige schnelle Konvertierung meiner Antwort, die tatsächlich funktioniert. In der Tat ist es neben meiner die einzige Antwort, die von den erstaunlichen 27 (meistens Müll-) Antworten, die diese Frage angezogen hat , überhaupt funktioniert . Ich habe dies bearbeitet, um festzustellen, dass es für Swift 2 funktioniert , und habe es auch als Grundlage für meinen eigenen Swift 4-Port verwendet . Ich wollte mich nur bedanken und dich wissen lassen!
Mark Amery

4

Hier ist eine Swift-Version für den Fall, dass dies für alle nützlich ist, die noch nach dieser Antwort suchen, aber Swift anstelle von Objective-C verwenden. Die Konzepte sind trotzdem gleich.

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
    //range.length will be greater than 0 if user is deleting text - allow it to replace
    if range.length > 0
    {
        return true
    }

    //Don't allow empty strings
    if string == " "
    {
        return false
    }

    //Check for max length including the spacers we added
    if range.location == 20
    {
        return false
    }

    var originalText = textField.text
    let replacementText = string.stringByReplacingOccurrencesOfString(" ", withString: "")

    //Verify entered text is a numeric value
    let digits = NSCharacterSet.decimalDigitCharacterSet()
    for char in replacementText.unicodeScalars
    {
        if !digits.longCharacterIsMember(char.value)
        {
            return false
        }
    }

    //Put an empty space after every 4 places
    if originalText!.length() % 5 == 0
    {
        originalText?.appendContentsOf(" ")
        textField.text = originalText
    }

    return true
}

schöner Code. Funktioniert für mich, hat aber ein Problem. Am Anfang der Zeichenfolge wird ein Leerzeichen eingefügt. Zum Beispiel, wenn ich 424242424242424242 schreiben möchte, dann wird aus dieser Zeichenfolge "4242 4242 4242 4242" ausgegeben.
Birju

-1; Neben dem von @Birju erwähnten Leerzeichen am Anfang ist dies auch fehlerhaft, wenn ich den Textcursor an eine frühere Position in der Zeichenfolge bewege. Wenn ich dort tippe, wird nicht nur der Abstand zwischen den Blöcken mit 4 Zahlen unterbrochen, sondern ich kann auch die Zeichenbeschränkung überschreiten.
Mark Amery

4

Um das Ziel der Formatierung des auf diese Weise in das Textfeld eingegebenen Textes zu erreichen, ist es wichtig, einige wichtige Dinge zu beachten. XXXX XXXX XXXX XXXX. Neben der Tatsache, dass die 16-stellige Kartennummer, die alle vier Ziffern getrennt wird, das am häufigsten verwendete Format ist, gibt es Karten mit 15 Ziffern (AmEx-formatiertes XXXX XXXXXX XXXXX) und andere mit 13 Ziffern oder sogar mit 19 Ziffern ( https: // en). wikipedia.org/wiki/Payment_card_number ). Eine andere wichtige Sache, die Sie berücksichtigen sollten, ist die Konfiguration des Textfelds, um nur Ziffern zuzulassen. Konfigurieren Sie den Tastaturtyp als numberPad. Dies ist ein guter Anfang, aber es ist praktisch, eine Methode zu implementieren, die die Eingabe sichert.

Ein Ausgangspunkt ist die Entscheidung, wann Sie die Nummer formatieren möchten, während der Benutzer die Nummer eingibt oder wann der Benutzer das Textfeld verlässt. Für den Fall, dass Sie formatieren möchten, wenn der Benutzer das textField verlässt, können Sie die Methode textFieldDidEndEditing (_ :) des Delegaten verwenden, um den Inhalt des textField zu übernehmen und zu formatieren.

Für den Fall, dass Sie, während der Benutzer die Nummer eingibt, die delegate-Methode textField (_: shouldChangeCharactersIn: replaceString :) nützlich ist, die aufgerufen wird, wenn sich der aktuelle Text ändert.

In beiden Fällen gibt es immer noch ein Problem. Finden Sie heraus, welches Format für die eingegebene Nummer das richtige ist. IMHO. Basierend auf allen Nummern, die ich gesehen habe, gibt es nur zwei Hauptformate: das oben beschriebene Amex-Format mit 15 Ziffern und das Formatieren Sie die Gruppenkartennummer alle vier Ziffern, wobei es egal ist, wie viele Ziffern vorhanden sind. In diesem Fall wird beispielsweise eine Karte mit 13 Ziffern wie XXXXX XXXX XXXX X formatiert und mit 19 Ziffern wie diese XXXX aussehen XXXX XXXX XXXX XXX, dies funktioniert für die häufigsten Fälle (16 Ziffern) und auch für die anderen. Sie können also herausfinden, wie Sie den AmEx-Fall mit demselben Algorithmus wie unten verwalten und mit den magischen Zahlen spielen.

Ich habe ein RegEx verwendet, um sicherzustellen, dass eine 15-stellige Karte bei anderen bestimmten Formaten ein amerikanischer Express ist

let regex = NSPredicate(format: "SELF MATCHES %@", "3[47][A-Za-z0-9*-]{13,}" )
let isAmex = regex.evaluate(with: stringToValidate)

Ich empfehle dringend, das spezifische RegEx zu verwenden, das nützlich ist, um den Emittenten zu identifizieren und herauszufinden, wie viele Ziffern akzeptiert werden sollten.

Jetzt ist mein schneller Ansatz für die Lösung mit textFieldDidEndEditing

func textFieldDidEndEditing(_ textField: UITextField) {

    _=format(cardNumber: textField.text!)

}
func format(cardNumber:String)->String{
    var formatedCardNumber = ""
    var i :Int = 0
    //loop for every character
    for character in cardNumber.characters{
        //in case you want to replace some digits in the middle with * for security
        if(i < 6 || i >= cardNumber.characters.count - 4){
            formatedCardNumber = formatedCardNumber + String(character)
        }else{
            formatedCardNumber = formatedCardNumber + "*"
        }
        //insert separators every 4 spaces(magic number)
        if(i == 3 || i == 7 || i == 11 || (i == 15 && cardNumber.characters.count > 16 )){
            formatedCardNumber = formatedCardNumber + "-"
            // could use just " " for spaces
        }

        i = i + 1
    }
    return formatedCardNumber
}

und für shouldChangeCharactersIn: replaceString: a Swift 3.0 von Jayesh Miruliya Answer setzen Sie ein Trennzeichen zwischen die Gruppe der vier Zeichen

 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
    {
        if textField == CardNumTxt
        {
            let replacementStringIsLegal = string.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789").inverted) == nil

        if !replacementStringIsLegal
        {
            return false
        }

        let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
        let components = newString.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted)

        let decimalString = components.joined(separator: "") as NSString
        let length = decimalString.length
        let hasLeadingOne = length > 0 && decimalString.character(at: 0) == (1 as unichar)

        if length == 0 || (length > 16 && !hasLeadingOne) || length > 19
        {
            let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int

            return (newLength > 16) ? false : true
        }
        var index = 0 as Int
        let formattedString = NSMutableString()

        if hasLeadingOne
        {
            formattedString.append("1 ")
            index += 1
        }
        if length - index > 4 //magic number separata every four characters
        {
            let prefix = decimalString.substring(with: NSMakeRange(index, 4))
            formattedString.appendFormat("%@-", prefix)
            index += 4
        }

        if length - index > 4
        {
            let prefix = decimalString.substring(with: NSMakeRange(index, 4))
            formattedString.appendFormat("%@-", prefix)
            index += 4
        }
        if length - index > 4
        {
            let prefix = decimalString.substring(with: NSMakeRange(index, 4))
            formattedString.appendFormat("%@-", prefix)
            index += 4
        }


        let remainder = decimalString.substring(from: index)
        formattedString.append(remainder)
        textField.text = formattedString as String
        return false
        }
        else
        {
            return true
        }
    }

Dieser Code sieht so beängstigend aus. Sie können ein "while" anstelle von 3 if-Anweisungen verwenden und viele nicht benötigte Variablen vermeiden
user3206558

-1; Wie Sie bemerken, wird der größte Teil des Codes hier nur aus der Antwort eines anderen Benutzers kopiert und eingefügt (und, wie ich bei dieser Antwort feststelle, funktioniert nicht), und der Rest beantwortet die Frage nicht.
Mark Amery

4

Swift 3.2

Kleine Korrektur in der @ Lucas-Antwort und im Arbeitscode in Swift 3.2. Entfernen Sie auch das Leerzeichen automatisch.

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

    if range.location == 19 {
        return false
    }

    if range.length == 1 {
        if (range.location == 5 || range.location == 10 || range.location == 15) {
            let text = textField.text ?? ""
            textField.text = text.substring(to: text.index(before: text.endIndex))
        }
        return true
    }

    if (range.location == 4 || range.location == 9 || range.location == 14) {
        textField.text = String(format: "%@ ", textField.text ?? "")
    }

    return true
}

1
-1; das ist total kaputt. Wenn ich den Cursor an eine andere Stelle als das Ende des Textfelds bewege und tippe, kann ich die Formatierung aufheben und die Längenbeschränkungen verletzen. Außerdem passiert wirklich seltsames Zeug, wenn ich den Cursor an eine Stelle bewege, die sich nicht am Ende des Textfelds befindet, und Zeichen lösche . ganze Brocken von Charakteren wurden am Ende gekaut.
Mark Amery

3

Definieren Sie die folgende Methode und rufen Sie sie in UITextfield-Delegaten oder wo immer erforderlich auf

-(NSString*)processString :(NSString*)yourString
{
    if(yourString == nil){
        return @"";
    }
    int stringLength = (int)[yourString length];
    int len = 4;  // Length after which you need to place added character
    NSMutableString *str = [NSMutableString string];
    int i = 0;
    for (; i < stringLength; i+=len) {
        NSRange range = NSMakeRange(i, len);
        [str appendString:[yourString substringWithRange:range]];
        if(i!=stringLength -4){
            [str appendString:@" "]; //If required string format is XXXX-XXXX-XXXX-XXX then just replace [str appendString:@"-"]
        }
    }
    if (i < [str length]-1) {  // add remaining part
        [str appendString:[yourString substringFromIndex:i]];
    }
    //Returning required string

    return str;
}

Mir ist nicht klar, wie Sie diese Methode verwenden möchten, und sie hat nichts mit der Positionierung des Textcursors zu tun. -1.
Mark Amery

2

Swift 3-Lösung basierend auf Mark Amerys Objective-C-Lösung :

  1. Implementieren Sie Aktionen und delegieren Sie Methoden:

    textField.addTarget(self, action: #selector(reformatAsCardNumber(_:))
    textField.delegate = self
    
  2. TextField Delegate-Methoden und andere Methoden:

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        previousTextFieldContent = textField.text;
        previousSelection = textField.selectedTextRange;
        return true
    }
    
    func reformatAsCardNumber(_ textField: UITextField) {
        var targetCursorPosition = 0
        if let startPosition = textField.selectedTextRange?.start {
            targetCursorPosition = textField.offset(from:textField.beginningOfDocument, to: startPosition)
        }
    
        var cardNumberWithoutSpaces = ""
        if let text = textField.text {
            cardNumberWithoutSpaces = removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
        }
    
        if cardNumberWithoutSpaces.characters.count > 19 {
            textField.text = previousTextFieldContent
            textField.selectedTextRange = previousSelection
            return
        }
    
        let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
        textField.text = cardNumberWithSpaces
    
        if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
            textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
        }
    }
    
    func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
        var digitsOnlyString = ""
        let originalCursorPosition = cursorPosition
    
        for i in stride(from: 0, to: string.characters.count, by: 1) {
            let characterToAdd =  string[string.index(string.startIndex, offsetBy: i)]
            if characterToAdd >= "0" && characterToAdd <= "9" {
                digitsOnlyString.append(characterToAdd)
            }
            else if i < originalCursorPosition {
                cursorPosition -= 1
            }
        }
    
        return digitsOnlyString
    }
    
    func insertSpacesEveryFourDigitsIntoString(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
        var stringWithAddedSpaces = ""
        let cursorPositionInSpacelessString = cursorPosition
    
        for i in stride(from: 0, to: string.characters.count, by: 1) {
            if i > 0 && (i % 4) == 0 {
                stringWithAddedSpaces.append(" ")
                if i < cursorPositionInSpacelessString {
                    cursorPosition += 1
                }
            }
            let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
            stringWithAddedSpaces.append(characterToAdd)
        }
    
        return stringWithAddedSpaces
    }
    

Ich habe die Zuordnung zu dieser Antwort bearbeitet, die meiner Ansicht nach auf meiner basiert (sie hat dieselben Variablen- und Methodennamen). Ich bin einigermaßen liberal in Bezug auf Urheberrechtsverletzungen und Plagiate, und ich denke, Sie haben vielleicht gedacht, dass die Zuschreibung hier keine große Sache ist, da meine Antwort buchstäblich auf derselben Seite ist, aber ohne expliziten Code direkt von einer Sprache in eine andere geschrieben wird Es scheint mir immer noch falsch zu sein, darauf hinzuweisen, dass es das ist, was es ist, oder auf die ursprüngliche Quelle zu verlinken (in einem Kontext, in dem das Zuschreiben trivial gewesen wäre). Ich habe aus diesem Grund eine -1 hinterlassen.
Mark Amery

2

Swift 5.1, Xcode 11

Nachdem ich viele Lösungen ausprobiert hatte, hatte ich Probleme wie das Setzen der richtigen Cursorposition und das Formatieren nach Bedarf. Nachdem ich zwei Beiträge kombiniert hatte ( https://stackoverflow.com/a/38838740/10579134 , https: // stackoverflow) , fand ich schließlich eine Lösung . com / a / 45297778/10579134 )

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    guard let currentText = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) else { return true }


    if textField == yourTextField  {

        textField.setText(to: currentText.grouping(every: 4, with: "-"), preservingCursor: true)

        return false
    }
    return true
}

Und diese Erweiterung hinzufügen

extension UITextField {

public func setText(to newText: String, preservingCursor: Bool) {
    if preservingCursor {
        let cursorPosition = offset(from: beginningOfDocument, to: selectedTextRange!.start) + newText.count - (text?.count ?? 0)
        text = newText
        if let newPosition = self.position(from: beginningOfDocument, offset: cursorPosition) {
            selectedTextRange = textRange(from: newPosition, to: newPosition)
        }
    }
    else {
        text = newText
    }
}

2

In Swift 5:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if textField == cardNumberTextField {
            return formatCardNumber(textField: textField, shouldChangeCharactersInRange: range, replacementString: string)
        }
        return true
    }


    func formatCardNumber(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
        if textField == cardNumberTextField {
            let replacementStringIsLegal = string.rangeOfCharacter(from: NSCharacterSet(charactersIn: "0123456789").inverted) == nil

            if !replacementStringIsLegal {
                return false
            }

            let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
            let components = newString.components(separatedBy: NSCharacterSet(charactersIn: "0123456789").inverted)
            let decimalString = components.joined(separator: "") as NSString
            let length = decimalString.length
            let hasLeadingOne = length > 0 && decimalString.character(at: 0) == (1 as unichar)

            if length == 0 || (length > 16 && !hasLeadingOne) || length > 19 {
                let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int

                return (newLength > 16) ? false : true
            }
            var index = 0 as Int
            let formattedString = NSMutableString()

            if hasLeadingOne {
                formattedString.append("1 ")
                index += 1
            }
            if length - index > 4 {
                let prefix = decimalString.substring(with: NSRange(location: index, length: 4))
                formattedString.appendFormat("%@ ", prefix)
                index += 4
            }

            if length - index > 4 {
                let prefix = decimalString.substring(with: NSRange(location: index, length: 4))
                formattedString.appendFormat("%@ ", prefix)
                index += 4
            }
            if length - index > 4 {
                let prefix = decimalString.substring(with: NSRange(location: index, length: 4))
                formattedString.appendFormat("%@ ", prefix)
                index += 4
            }

            let remainder = decimalString.substring(from: index)
            formattedString.append(remainder)
            textField.text = formattedString as String
            return false
        } else {
            return true
        }
    }

1

Hier ist eine schnelle Kopie der akzeptierten Antwort, falls jemand sie benötigt. Es ist im Grunde eine Wrapper-Klasse. Ich habe nicht zu viel Zeit damit verbracht, es zu optimieren, aber es ist einsatzbereit.

var creditCardFormatter : CreditCardFormatter
{
    return CreditCardFormatter.sharedInstance
}

class CreditCardFormatter : NSObject
{
    static let sharedInstance : CreditCardFormatter = CreditCardFormatter()

    func formatToCreditCardNumber(textField : UITextField, withPreviousTextContent previousTextContent : String?, andPreviousCursorPosition previousCursorSelection : UITextRange?)
    {
        if let selectedRangeStart = textField.selectedTextRange?.start, textFieldText = textField.text
        {
            var targetCursorPosition : UInt = UInt(textField.offsetFromPosition(textField.beginningOfDocument, toPosition: selectedRangeStart))

            let cardNumberWithoutSpaces : String = removeNonDigitsFromString(textFieldText, andPreserveCursorPosition: &targetCursorPosition)

            if cardNumberWithoutSpaces.characters.count > 19
            {
                textField.text = previousTextContent
                textField.selectedTextRange = previousCursorSelection
                return
            }

            let cardNumberWithSpaces : String = insertSpacesIntoEvery4DigitsIntoString(cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)

            textField.text = cardNumberWithSpaces

            if let finalCursorPosition = textField.positionFromPosition(textField.beginningOfDocument, offset: Int(targetCursorPosition))
            {
                textField.selectedTextRange = textField.textRangeFromPosition(finalCursorPosition, toPosition: finalCursorPosition)
            }
        }
    }

    func removeNonDigitsFromString(string : String,inout andPreserveCursorPosition cursorPosition : UInt) -> String
    {
        var digitsOnlyString : String = ""

        for index in 0.stride(to: string.characters.count, by: 1)
        {
            let charToAdd : Character = Array(string.characters)[index]

            if isDigit(charToAdd)
            {
                digitsOnlyString.append(charToAdd)
            }
            else
            {
                if index < Int(cursorPosition)
                {
                    cursorPosition -= 1
                }
            }
        }

        return digitsOnlyString
    }

    private func isDigit(character : Character) -> Bool
    {
        return "\(character)".containsOnlyDigits()
    }

    func insertSpacesIntoEvery4DigitsIntoString(string : String, inout andPreserveCursorPosition cursorPosition : UInt) -> String
    {
        var stringWithAddedSpaces : String = ""

        for index in 0.stride(to: string.characters.count, by: 1)
        {
            if index != 0 && index % 4 == 0
            {
                stringWithAddedSpaces += " "

                if index < Int(cursorPosition)
                {
                    cursorPosition += 1
                }
            }

            let characterToAdd : Character = Array(string.characters)[index]

            stringWithAddedSpaces.append(characterToAdd)
        }

        return stringWithAddedSpaces
    }

}

extension String
{
    func containsOnlyDigits() -> Bool
    {
        let notDigits : NSCharacterSet = NSCharacterSet.decimalDigitCharacterSet().invertedSet

        if (rangeOfCharacterFromSet(notDigits, options: NSStringCompareOptions.LiteralSearch, range: nil) == nil)
        {
            return true
        }

        return false
    }
}

1
-1; Sie setzen previousTextContentnirgendwo, also empfängt es nil(oder, wenn Sie es zu einem Stringanstelle von a machen String?, empfängt es einige zufällige Müllbytes). Das heißt, wenn Sie 19 Zeichen überlaufen, wird das gesamte Textfeld nur ausgeblendet (oder die App stürzt sofort ab, wenn Sie Pech haben - aber bisher habe ich die Ausblendung immer gesehen).
Mark Amery

2
@MarkAmery Ich bewundere Ihre harte Arbeit und Pedant-Analyse jeder Lösung aus diesem Beitrag :) Dies war, wie erwähnt, nur eine schnelle Lösung, die möglicherweise einige Randfälle nicht berücksichtigt. Dies ermutigt die Leute, nicht nur die auf dem Stapel gefundenen Lösungen zu kopieren und einzufügen, sondern Verstehe auch und komme mit verbesserten Antworten. ˆ.ˆ Hab auch einen guten Tag (;
Fawkes

1

Diese Antworten sind einfach viel zu viel Code für mich. Hier ist eine Lösung in Swift 2.2.1

extension UITextField {

    func setText(to newText: String, preservingCursor: Bool) {
        if preservingCursor {
            let cursorPosition = offsetFromPosition(beginningOfDocument, toPosition: selectedTextRange!.start) + newText.characters.count - (text?.characters.count ?? 0)
            text = newText
            if let newPosition = positionFromPosition(beginningOfDocument, offset: cursorPosition) {
                selectedTextRange = textRangeFromPosition(newPosition, toPosition: newPosition)
            }
        }
        else {
            text = newText
        }
    }
}

Fügen Sie jetzt einfach eine IBAction in Ihren View Controller ein:

@IBAction func textFieldEditingChanged(sender: UITextField) {
    var digits = current.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet).joinWithSeparator("") // remove non-digits
    // add spaces as necessary or otherwise format your digits.
    // for example for a phone number or zip code or whatever
    // then just:
    sender.setText(to: digits, preservingCursor: true)
}

-1; Abgesehen davon, dass dies dem Leser eine Menge Arbeit überlässt (und nicht so kompiliert, wie es ist; was currentsoll das sein?), Wird die Cursorposition nicht richtig beibehalten. zB wenn ich 1234 5678in das Textfeld habe und ein 0nach dem 4tippe, ende ich mit 1234 5678 0aber mit meinem Cursor vor dem 0ich gerade getippt habe, anstatt danach.
Mark Amery

1

Hier ist eine Kotlin-Antwort von Mark Amery

fun formatCardNumber(cardNumber: String): String {
    var trimmedCardNumber = cardNumber.replace(" ","")

    // UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
    val is456 = trimmedCardNumber.startsWith("1")

    // These prefixes reliably indicate either a 4-6-5 or 4-6-4 card. We treat all these
    // as 4-6-5-4 to err on the side of always letting the user type more digits.
    val is465 = listOf("34", "37", "300", "301", "302", "303", "304", "305", "309", "36", "38", "39")
            .any { trimmedCardNumber.startsWith(it) }

    // In all other cases, assume 4-4-4-4.
    val is4444 = !(is456 || is465)

    trimmedCardNumber = if (is456 || is465) {
         trimmedCardNumber.take(cardNumberMaxLengthAmex)
    } else {
         trimmedCardNumber.take(cardNumberMaxLength)
    }

    var cardNumberWithAddedSpaces = ""

    trimmedCardNumber.forEachIndexed { index, c ->
        val needs465Spacing = is465 && (index == 4 || index == 10 || index == 15)
        val needs456Spacing = is456 && (index == 4 || index == 9 || index == 15)
        val needs4444Spacing = is4444 && index > 0 && index % 4 == 0

        if (needs465Spacing || needs456Spacing || needs4444Spacing) {
            cardNumberWithAddedSpaces += " "
        }

        cardNumberWithAddedSpaces += c
    }

    return cardNumberWithAddedSpaces
}

Fügen Sie dann einen Textänderungs-Listener zu einem Bearbeitungstext hinzu

var flag = false

editText.addTextChangedListener(object : TextWatcher {
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            if (flag) { 
                flag = false 
            } else {
                val text = formatCardNumber(s.toString())
                flag = true
                editText.setText(text)
                editText.setSelection(text.count())
            }
        }

        override fun afterTextChanged(s: Editable?) {}
    })

1

Sie können meine einfache Bibliothek verwenden: DECardNumberFormatter

Beispiel:

// You can use it like default UITextField
let textField = DECardNumberTextField()
// Custom required setup
textField.setup()

Ausgabe:

For sample card number (Visa) 4111111111111111
Format (4-4-4-4): 4111 1111 1111 1111

For sample card number (AmEx) 341212345612345
Format (4-6-5): 3412 123456 12345

1

schnell 5

Hallo, hier ist die schnelle 5-Version von Mark Amerys akzeptierter Antwort , die für mich funktioniert hat

Fügen Sie diese Variablen Ihrer Klasse hinzu.

@IBOutlet weak var cardNumberTextField: UITextField!
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?

Stellen Sie außerdem sicher, dass Ihr Textfeld reformatAsCardNumber aufruft: from viewDidLoad ()

cardNumberTextField.addTarget(self, action: #selector(reformatAsCardNumber), for: .editingChanged)

Fügen Sie dies in Ihr UITextFieldDelegate ein

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
   
    if textField == cardNumberTextField {
        previousTextFieldContent = textField.text;
        previousSelection = textField.selectedTextRange;
    }
    
    return true
}

Fügen Sie zuletzt die folgenden Methoden in Ihren viewController ein:

@objc func reformatAsCardNumber(textField: UITextField) {
    var targetCursorPosition = 0
    if let startPosition = textField.selectedTextRange?.start {
        targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition)
    }
    
    var cardNumberWithoutSpaces = ""
    if let text = textField.text {
        cardNumberWithoutSpaces = self.removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
    }
    
    if cardNumberWithoutSpaces.count > 19 {
        textField.text = previousTextFieldContent
        textField.selectedTextRange = previousSelection
        return
    }
    
    let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
    textField.text = cardNumberWithSpaces
    
    if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
        textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
    }
}

func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
    var digitsOnlyString = ""
    let originalCursorPosition = cursorPosition
    
    for i in Swift.stride(from: 0, to: string.count, by: 1) {
        let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
        if characterToAdd >= "0" && characterToAdd <= "9" {
            digitsOnlyString.append(characterToAdd)
        }
        else if i < originalCursorPosition {
            cursorPosition -= 1
        }
    }
    
    return digitsOnlyString
}

func insertSpacesEveryFourDigitsIntoString(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
    var stringWithAddedSpaces = ""
    let cursorPositionInSpacelessString = cursorPosition
    
    for i in Swift.stride(from: 0, to: string.count, by: 1) {
        if i > 0 && (i % 4) == 0 {
            stringWithAddedSpaces.append(contentsOf: " ")
            if i < cursorPositionInSpacelessString {
                cursorPosition += 1
            }
        }
        let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
        stringWithAddedSpaces.append(characterToAdd)
    }
    
    return stringWithAddedSpaces
}

0

Bitte verwenden Sie eine einfache Form der Kreditkarte / ** Siehe Verwendungsbeispiel: ### let str = "41111111111111111"

 let x = yourClassname.setStringAsCardNumberWithSartNumber(4, withString: str!, withStrLenght: 8)

 ### output:- 4111XXXXXXXX1111

 let x = yourClassname.setStringAsCardNumberWithSartNumber(0, withString: str!, withStrLenght: 12)

 ### output: - XXXXXXXXXXXX1111

 */
func setStringAsCardNumberWithSartNumber(Number:Int,withString str:String ,withStrLenght len:Int ) -> String{
    //let aString: String = "41111111111111111"
    let arr = str.characters
    var CrediteCard : String = ""
    if arr.count > (Number + len) {
        for (index, element ) in arr.enumerate(){
            if index >= Number && index < (Number + len) {
                CrediteCard = CrediteCard + String("X")
            }else{
                CrediteCard = CrediteCard + String(element)
            }
        }
      return CrediteCard
    }else{
            print("\(Number) plus \(len) are grether than strings chatarter \(arr.count)")
    }
    print("\(CrediteCard)")
    return str
}

Ich hoffe das ist hilfreich für dich.


-1 für die Rechtschreibfehler und den kommentierten Code; Sie wecken nicht gerade das Vertrauen, dass dies irgendetwas Vernünftiges bewirken wird.
Mark Amery

0

Ich habe die Antwort von @ilesh so geändert, dass nur die letzten 4 Ziffern angezeigt werden, unabhängig von der Länge. Auch um das Leerzeichen und "-" Zeichen zu ignorieren. Auf diese Weise wird bei einer Nummer mit dem Format 0000 - 0000 - 0000 - 0000 XXXX - XXXX - XXXX - 0000 angezeigt

func setStringAsCardNumberWithSartNumber(Number:Int,withString str:String) -> String{
    let arr = str.characters
    var CrediteCard : String = ""
    let len = str.characters.count-4
    if arr.count > (Number + len) {
        for (index, element ) in arr.enumerated(){
            if index >= Number && index < (Number + len) && element != "-" && element != " " {
                CrediteCard = CrediteCard + String("X")
            }else{
                CrediteCard = CrediteCard + String(element)
            }
        }
        return CrediteCard
    }else{
        print("\(Number) plus \(len) are grether than strings chatarter \(arr.count)")
    }
    print("\(CrediteCard)")
    return str
}

-1 für das Fehlen von Gebrauchsanweisungen und das gebrochene Englisch. Diese Antwort erwähnt, wie die von ilesh, nicht einmal UITextFields.
Mark Amery

0

In Github wurde ein GIST gefunden, der genau das tut, was ich in Swift3 brauche ( https://gist.github.com/nunogoncalves/6a8b4b21f4f69e0fc050190df96a1e56 ).

Implementiert durch tun ->

if creditCardNumberTextView.text?.characters.first == "3" {
    let validator = Validator(cardType: .americanExpress, value:  self.creditCardNumberTextView.text!).test()

      if validator == true {

       } else {

       }
   }

Funktioniert wunderbar in der App, die ich ausarbeite und die Kreditkarten verwendet.


-1; Dieses Gist dient zur Validierung von Kartennummern und nicht zur Formatierung. Dies ist keine Antwort auf die gestellte Frage.
Mark Amery

0

In meinem Fall müssen wir die iban-Nummer formulieren. Ich denke, der folgende Codeblock hilft Ihnen

Überprüfen Sie zunächst, ob der vom Benutzer eingegebene Wert gültig ist

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{

    if(textField == self.ibanTextField){

           BOOL shouldChange =  ([Help checkTextFieldForIBAN:[NSString stringWithFormat:@"%@%@",textField.text,string]]);
 }
}

Zweitens können Sie die iban-formatierte Methode wie unten sehen. Unser iban formatierter Anfangsbuchstabe 2.

+(BOOL)checkTextFieldForIBAN:(NSString*)string{

    string = [string stringByReplacingOccurrencesOfString:@" " withString:@""];

    if ([string length] <= 26) {

        if ([string length] > 2) {

            if ([self isLetter:[string substringToIndex:2]]) {

                if ([self isInteger:[string substringFromIndex:2]])
                    return YES;
                else
                    return NO;

            }else {

                return NO;
            }
        }else{

            return [self isLetter:string];
        }

    }
    else {

        return NO;
    }

    return YES;
}

0

Hier ist die Änderung der Antwort von @sleeping_giant für schnell. Diese Lösung formatiert den Text im Format 'xxxx-xxxx-xxxx-xxxx-xxxx' und akzeptiert keine Zahlen mehr außerhalb dieses Bereichs.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
{
    if string == ""{
        return true
    }

    //range.length will be greater than 0 if user is deleting text - allow it to replace
    if range.length > 0
    {
        return true
    }

    //Don't allow empty strings
    if string == "-"
    {
        return false
    }

    //Check for max length including the spacers we added
    print(range.location)
    if range.location > 23
    {
        return false
    }

    var originalText = textField.text
    let replacementText = string.replacingOccurrences(of: "-", with: "")

    //Verify entered text is a numeric value
    let digits = NSCharacterSet.decimalDigits
    for char in replacementText.unicodeScalars
    {
        if !(digits as NSCharacterSet).longCharacterIsMember(char.value)
        {
            return false
        }
    }

    //Put an empty space after every 4 places
    if (originalText?.characters.count)! > 0
    {
        if (originalText?.characters.count)! < 5 && (originalText?.characters.count)! % 4 == 0{
            originalText?.append("-")
        }else if(((originalText?.characters.count)! + 1) % 5 == 0){
            originalText?.append("-")
        }

    }

    textField.text = originalText

    return true
}

-1; Wie viele der Antworten hier bricht dies vollständig ab, wenn ich meinen Textcursor links neben das Textfeld bewege und dort einige Ziffern einfüge.
Mark Amery

0

Schauen Sie sich diese Lösung an. Ich fand in Autorize.net SDK Beispiel.

Machen Sie Ihren UITextFieldTastaturtyp zu Numeric.

Es maskiert Kreditkartennummern mit 'X' und durch Hinzufügen von Leerzeichen wird das 'XXXX XXXX XXXX 1234'Format erstellt.

In der Header-Datei .h

    #define kSpace @" "
    #define kCreditCardLength 16
    #define kCreditCardLengthPlusSpaces (kCreditCardLength + 3)
    #define kCreditCardObscureLength (kCreditCardLength - 4)

    @property (nonatomic, strong) NSString *creditCardBuf;
    IBOutlet UITextField *txtCardNumber;

In der .m-Datei

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    if (textField == txtCardNumber) {
        if ([string length] > 0) { //NOT A BACK SPACE Add it

            if ([self isMaxLength:textField])
                return NO;

            self.creditCardBuf  = [NSString stringWithFormat:@"%@%@", self.creditCardBuf, string];
        } else {

            //Back Space do manual backspace
            if ([self.creditCardBuf length] > 1) {
                self.creditCardBuf = [self.creditCardBuf substringWithRange:NSMakeRange(0, [self.creditCardBuf length] - 1)];
            } else {
                self.creditCardBuf = @"";
            }
        }
        [self formatValue:textField];
    }

    return NO;
}

- (BOOL) isMaxLength:(UITextField *)textField {

    if (textField == txtCardNumber && [textField.text length] >= kCreditCardLengthPlusSpaces) {
        return YES;
    }
    return NO;
}

- (void) formatValue:(UITextField *)textField {
    NSMutableString *value = [NSMutableString string];

    if (textField == txtCardNumber) {
        NSInteger length = [self.creditCardBuf length];

        for (int i = 0; i < length; i++) {

            // Reveal only the last character.
            if (length <= kCreditCardObscureLength) {

                if (i == (length - 1)) {
                    [value appendString:[self.creditCardBuf substringWithRange:NSMakeRange(i,1)]];
                } else {
                    [value appendString:@“X”];
                }
            }
            // Reveal the last 4 characters
            else {

                if (i < kCreditCardObscureLength) {
                    [value appendString:@“X”];
                } else {
                    [value appendString:[self.creditCardBuf substringWithRange:NSMakeRange(i,1)]];
                }
            }

            //After 4 characters add a space
            if ((i +1) % 4 == 0 &&
                ([value length] < kCreditCardLengthPlusSpaces)) {
                [value appendString:kSpace];
            }
        }
        textField.text = value;
    }
}

-1; das ist hoffnungslos kaputt. Abgesehen von den magischen Anführungszeichen, die es zu einem Syntaxfehler machen - da ich diese zumindest leicht beheben kann -, wird beim ersten Eingeben Xdie Zeichenfolge (nul l)in den Anfang des Textfelds eingefügt (mit s maskiert ) und wenn ich ein Zeichen eingebe In der Mitte der Kartennummer springt mein Cursor zum Ende.
Mark Amery

0

Bitte überprüfen Sie die unten stehende Lösung, sie funktioniert gut für mich.

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

        let subString = (textField.text as! NSString).substringWithRange(range)
        if subString == " " && textField == cardNumberTextfield
        {
            return false     // user should not be able to delete space from card field
        }
        else if string == ""
        {
            return true      // user can delete any digit
        }


        // Expiry date formatting

        if textField == expiryDateTextfield
        {
            let str = textField.text! + string

            if str.length == 2 && Int(str) > 12
            {
                return false                  // Month should be <= 12
            }
            else if str.length == 2
            {
                textField.text = str+"/"      // append / after month
                return false
            }
            else if str.length > 5
            {
                return false                  // year should be in yy format
            }
        }



        // Card number formatting

        if textField == cardNumberTextfield
        {
            let str = textField.text! + string

            let stringWithoutSpace = str.stringByReplacingOccurrencesOfString(" ", withString: "")

            if stringWithoutSpace.length % 4 == 0 && (range.location == textField.text?.length)
            {
                if stringWithoutSpace.length != 16
                {
                    textField.text = str+" "    // add space after every 4 characters
                }
                else
                {
                    textField.text = str       // space should not be appended with last digit
                }

                return false
            }
            else if str.length > 19
            {
                return false
            }
        }



        return true
    }

-1; Abgesehen von der fehlerhaften Formatierung und dem fehlerhaften Englisch des Posts und der Tatsache, dass hier eine Menge irrelevanten Codes zu Ablaufdaten vorhanden ist, funktioniert dies nicht. Wenn Sie den Textcursor an eine andere Stelle als den Punkt ganz rechts im Textfeld bewegen und eingeben, wird die Formatierung unterbrochen.
Mark Amery

0

Erstellen Sie eine neue schnelle Datei und fügen Sie sie unter den Code ein. Ändern Sie die Textfeldklasse in VSTextField

import UIKit

public enum TextFieldFormatting {
    case uuid
    case socialSecurityNumber
    case phoneNumber
    case custom
    case noFormatting
}

public class VSTextField: UITextField {

    /**
     Set a formatting pattern for a number and define a replacement string. For example: If formattingPattern would be "##-##-AB-##" and
     replacement string would be "#" and user input would be "123456", final string would look like "12-34-AB-56"
     */
    public func setFormatting(_ formattingPattern: String, replacementChar: Character) {
        self.formattingPattern = formattingPattern
        self.replacementChar = replacementChar
        self.formatting = .custom
    }

    /**
     A character which will be replaced in formattingPattern by a number
     */
    public var replacementChar: Character = "*"

    /**
     A character which will be replaced in formattingPattern by a number
     */
    public var secureTextReplacementChar: Character = "\u{25cf}"

    /**
     True if input number is hexadecimal eg. UUID
     */
    public var isHexadecimal: Bool {
        return formatting == .uuid
    }

    /**
     Max length of input string. You don't have to set this if you set formattingPattern.
     If 0 -> no limit.
     */
    public var maxLength = 0

    /**
     Type of predefined text formatting. (You don't have to set this. It's more a future feature)
     */
    public var formatting : TextFieldFormatting = .noFormatting {
        didSet {
            switch formatting {

            case .socialSecurityNumber:
                self.formattingPattern = "***-**-****"
                self.replacementChar = "*"

            case .phoneNumber:
                self.formattingPattern = "***-***-****"
                self.replacementChar = "*"

            case .uuid:
                self.formattingPattern = "********-****-****-****-************"
                self.replacementChar = "*"

            default:
                self.maxLength = 0
            }
        }
    }

    /**
     String with formatting pattern for the text field.
     */
    public var formattingPattern: String = "" {
        didSet {
            self.maxLength = formattingPattern.count
        }
    }

    /**
     Provides secure text entry but KEEPS formatting. All digits are replaced with the bullet character \u{25cf} .
     */
    public var formatedSecureTextEntry: Bool {
        set {
            _formatedSecureTextEntry = newValue
            super.isSecureTextEntry = false
        }

        get {
            return _formatedSecureTextEntry
        }
    }

    override public var text: String! {
        set {
            super.text = newValue
            textDidChange() // format string properly even when it's set programatically
        }

        get {
            if case .noFormatting = formatting {
                return super.text
            } else {
                // Because the UIControl target action is called before NSNotificaion (from which we fire our custom formatting), we need to
                // force update finalStringWithoutFormatting to get the latest text. Otherwise, the last character would be missing.
                textDidChange()
                return finalStringWithoutFormatting
            }
        }
    }

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        registerForNotifications()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        registerForNotifications()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    /**
     Final text without formatting characters (read-only)
     */
    public var finalStringWithoutFormatting : String {
        return _textWithoutSecureBullets.keepOnlyDigits(isHexadecimal: isHexadecimal)
    }

    // MARK: - INTERNAL
    fileprivate var _formatedSecureTextEntry = false

    // if secureTextEntry is false, this value is similar to self.text
    // if secureTextEntry is true, you can find final formatted text without bullets here
    fileprivate var _textWithoutSecureBullets = ""

    fileprivate func registerForNotifications() {
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(VSTextField.textDidChange),
                                               name: NSNotification.Name(rawValue: "UITextFieldTextDidChangeNotification"),
                                               object: self)
    }

    @objc public func textDidChange() {
        var superText: String { return super.text ?? "" }

        // TODO: - Isn't there more elegant way how to do this?
        let currentTextForFormatting: String

        if superText.count > _textWithoutSecureBullets.count {
            currentTextForFormatting = _textWithoutSecureBullets + superText[superText.index(superText.startIndex, offsetBy: _textWithoutSecureBullets.count)...]
        } else if superText.count == 0 {
            _textWithoutSecureBullets = ""
            currentTextForFormatting = ""
        } else {
            currentTextForFormatting = String(_textWithoutSecureBullets[..<_textWithoutSecureBullets.index(_textWithoutSecureBullets.startIndex, offsetBy: superText.count)])
        }

        if formatting != .noFormatting && currentTextForFormatting.count > 0 && formattingPattern.count > 0 {
            let tempString = currentTextForFormatting.keepOnlyDigits(isHexadecimal: isHexadecimal)

            var finalText = ""
            var finalSecureText = ""

            var stop = false

            var formatterIndex = formattingPattern.startIndex
            var tempIndex = tempString.startIndex

            while !stop {
                let formattingPatternRange = formatterIndex ..< formattingPattern.index(formatterIndex, offsetBy: 1)
                if formattingPattern[formattingPatternRange] != String(replacementChar) {

                    finalText = finalText + formattingPattern[formattingPatternRange]
                    finalSecureText = finalSecureText + formattingPattern[formattingPatternRange]

                } else if tempString.count > 0 {

                    let pureStringRange = tempIndex ..< tempString.index(tempIndex, offsetBy: 1)

                    finalText = finalText + tempString[pureStringRange]

                    // we want the last number to be visible
                    if tempString.index(tempIndex, offsetBy: 1) == tempString.endIndex {
                        finalSecureText = finalSecureText + tempString[pureStringRange]
                    } else {
                        finalSecureText = finalSecureText + String(secureTextReplacementChar)
                    }

                    tempIndex = tempString.index(after: tempIndex)
                }

                formatterIndex = formattingPattern.index(after: formatterIndex)

                if formatterIndex >= formattingPattern.endIndex || tempIndex >= tempString.endIndex {
                    stop = true
                }
            }

            _textWithoutSecureBullets = finalText

            let newText = _formatedSecureTextEntry ? finalSecureText : finalText
            if newText != superText {
                super.text = _formatedSecureTextEntry ? finalSecureText : finalText
            }
        }

        // Let's check if we have additional max length restrictions
        if maxLength > 0 {
            if superText.count > maxLength {
                super.text = String(superText[..<superText.index(superText.startIndex, offsetBy: maxLength)])
                _textWithoutSecureBullets = String(_textWithoutSecureBullets[..<_textWithoutSecureBullets.index(_textWithoutSecureBullets.startIndex, offsetBy: maxLength)])
            }
        }
    }
}


extension String {

    func keepOnlyDigits(isHexadecimal: Bool) -> String {
        let ucString = self.uppercased()
        let validCharacters = isHexadecimal ? "0123456789ABCDEF" : "0123456789"
        let characterSet: CharacterSet = CharacterSet(charactersIn: validCharacters)
        let stringArray = ucString.components(separatedBy: characterSet.inverted)
        let allNumbers = stringArray.joined(separator: "")
        return allNumbers
    }
}


// Helpers
fileprivate func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
    switch (lhs, rhs) {
    case let (l?, r?):
        return l < r
    case (nil, _?):
        return true
    default:
        return false
    }
}

fileprivate func > <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
    switch (lhs, rhs) {
    case let (l?, r?):
        return l > r
    default:
        return rhs < lhs
    }
}

Weitere Verwendungszwecke finden Sie unter folgendem Link

Vielen Dank an den Mann, der die großartige Lösung für die Formatierung von Text in UITextField bereitgestellt hat.

http://vojtastavik.com/2015/03/29/real-time-formatting-in-uitextfield-swift-basics/

https://github.com/VojtaStavik/VSTextField

Arbeitet großartig für mich.


1
-1; neben der Tatsache , dass der Code hier kopiert und aus einer Bibliothek eingefügt , ohne als solche deutlich gekennzeichnet, es beantwortet auch die Frage nicht (oder sogar erwähnen Kreditkarten, tatsächlich) und die Bibliothek , die Sie kopiert es aus hat erhebliche Fehler , dass mein Antwort hat nicht.
Mark Amery

@MarkAmery Sie haben das Recht, abzustimmen. Aber vor kurzem habe ich diese Bibliothek benutzt und fand sie hilfreich. Möge jemand anderes dies hilfreich finden.
Haripal Wagh
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.