Input-mask-ios: ํŽธ์ง‘ ํ›„ ์ž˜๋ชป๋œ ๋งˆ์Šคํฌ๊ฐ€ ์ฒจ๋ถ€๋จ

์— ๋งŒ๋“  2018๋…„ 07์›” 25์ผ  ยท  11์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: RedMadRobot/input-mask-ios

๋‹ค์Œ ๋งˆ์Šคํฌ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
self.login.affineFormats = [ "{+7} ([000]) [000] [00] [00]", "{8} ([000]) [000]-[00]-[00]" ]

+7 ๋งˆ์Šคํฌ๊ฐ€ ์žˆ๋Š” ์ „ํ™”๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•œ ๋‹ค์Œ ([000]) ๋‚ด์˜ ๊ฐ’์„ ํŽธ์ง‘ํ•˜๋ฉด ๋งˆ์Šคํฌ๊ฐ€ +7 ๋Œ€์‹  8๋กœ ์ „ํ™˜๋˜๊ณ  ๊ทธ ๋‹ค์Œ 7์ด ([000]) ์•ˆ์— ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค

enhancement

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

๋„ค ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

๋ชจ๋“  11 ๋Œ“๊ธ€

๋งˆ์Šคํฌ๋กœ ์ดˆ๊ธฐํ™”๋˜๋ฉด textField์—์„œ ๋งˆ์Šคํฌ๋ฅผ "์ž ๊ทธ๋Š”" ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๊นŒ?

์•ˆ๋…•ํ•˜์„ธ์š” @MrJox , ๋ณด๊ณ ํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.
์ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ํ…์ŠคํŠธ ํ•„๋“œ์— ์žˆ๋Š” ํ…์ŠคํŠธ์˜ ์ •ํ™•ํ•œ ์ƒํƒœ๋ฅผ ์•Œ๋ ค์ฃผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

์˜ค๋Š˜ ๋‚˜์ค‘์— ์ถ”์ ํ•œ ๋‹ค์Œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ๋„์™€๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

์ƒํƒœ๊ฐ€ ๋ฌด์—‡์„ ์˜๋ฏธํ•˜๋Š”์ง€ ์ ˆ๋Œ€์ ์œผ๋กœ ํ™•์‹ ํ•  ์ˆ˜ ์—†์ง€๋งŒ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ผ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์บ์‹œ์—์„œ ์ „ํ™”๋ฒˆํ˜ธ๋ฅผ ์ฝ๊ณ  textField์— ์‚ฝ์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค(์‚ฌ์šฉ์ž ์ง€์ • JVFloat textField์ž…๋‹ˆ๋‹ค).
์ „ํ™”๋ฒˆํ˜ธ๋Š” +7 (926) 000-00-00 ์ธ๋ฐ '2' ๋’ค์— ํฌ์ธํ„ฐ๋ฅผ ๋†“๊ณ  ์ด ๋ฒˆํ˜ธ๋ฅผ ์ง€์›๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ ๋‚ด textView ์ฝ˜ํ…์ธ ๋Š” 8 (796) 000-00-00์œผ๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์–ด๋–ค ์ด์œ ๋กœ +๋ฅผ ์‚ญ์ œํ•˜๊ณ  7์„ ๋งˆ์Šคํฌ๊ฐ€ ์•„๋‹Œ ์ „ํ™” ๋ฒˆํ˜ธ์˜ ์‹ค์ œ ๋ถ€๋ถ„์œผ๋กœ ์ทจ๊ธ‰ํ•˜๊ณ  ๋Œ€๊ด„ํ˜ธ์— ๋„ฃ์€ ๋‹ค์Œ 8 ๋งˆ์Šคํฌ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ, ๊ท€ํ•˜์˜ ์„ค๋ช…์€ ์ €์—๊ฒŒ ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค. ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!
๊ณ„์† ์ง€์ผœ๋ด ์ฃผ์„ธ์š”.

@MrJox ๋งˆ์ง€๋ง‰ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—…๋ฐ์ดํŠธ์— ํ•„์š”ํ•œ ๋ช‡ ๊ฐ€์ง€ ๊ฐœ์„  ์‚ฌํ•ญ๊ณผ ์ˆ˜์ • ์‚ฌํ•ญ์„ ์ž‘์—…ํ•˜๋Š” ๋™์•ˆ ๋ฌธ์ œ์— ๋Œ€ํ•œ ๋น ๋ฅธ ์†”๋ฃจ์…˜์„ ๋งˆ๋ จํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์‹ค์ƒ ์‚ฌ์šฉ์ž ์ง€์ • ์„ ํ˜ธ๋„ ๊ณ„์‚ฐ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•œ PolyMaskTextFieldDelegate ์ˆ˜์ •์ž…๋‹ˆ๋‹ค. ๋งˆ์Šคํฌ์™€์˜ ์ „์ฒด ํ…์ŠคํŠธ ์„ ํ˜ธ๋„ ๋Œ€์‹  ์ด ๋ฉ”์„œ๋“œ๋Š” ์ ‘๋‘์‚ฌ ๊ต์ฐจ ๊ธธ์ด, ๋ฌธ์ž ์ˆ˜๋ฅผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค.

์ด ์ฝ”๋“œ ๋˜๋Š” ์ˆ˜์ •๋œ ๋ฒ„์ „์€ ๊ฒฐ๊ตญ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์†Œ์Šค์— ๋„๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import Foundation
import UIKit

import InputMask

<strong i="10">@IBDesignable</strong>
class PrefixAffinityMaskedTextFieldDelegate: MaskedTextFieldDelegate {
    public var affineFormats: [String]

    public init(primaryFormat: String, affineFormats: [String]) {
        self.affineFormats = affineFormats
        super.init(format: primaryFormat)
    }

    public override init(format: String) {
        self.affineFormats = []
        super.init(format: format)
    }

    override open func put(text: String, into field: UITextField) {
        let mask: Mask = pickMask(
            forText: text,
            caretPosition: text.endIndex,
            autocomplete: autocomplete
        )

        let result: Mask.Result = mask.apply(
            toText: CaretString(
                string: text,
                caretPosition: text.endIndex
            ),
            autocomplete: autocomplete
        )

        field.text = result.formattedText.string
        field.caretPosition = result.formattedText.string.distance(
            from: result.formattedText.string.startIndex,
            to: result.formattedText.caretPosition
        )

        listener?.textField?(field, didFillMandatoryCharacters: result.complete, didExtractValue: result.extractedValue)
    }

    override open func deleteText(inRange range: NSRange, inTextInput field: UITextInput) -> Mask.Result {
        let text: String = replaceCharacters(
            inText: field.allText,
            range: range,
            withCharacters: ""
        )

        let mask: Mask = pickMask(
            forText: text,
            caretPosition: text.index(text.startIndex, offsetBy: range.location),
            autocomplete: false
        )

        let result: Mask.Result = mask.apply(
            toText: CaretString(
                string: text,
                caretPosition: text.index(text.startIndex, offsetBy: range.location)
            ),
            autocomplete: false
        )

        field.allText = result.formattedText.string
        field.caretPosition = range.location

        return result
    }

    override open func modifyText(
        inRange range: NSRange,
        inTextInput field: UITextInput,
        withText text: String
    ) -> Mask.Result {
        let updatedText: String = replaceCharacters(
            inText: field.allText,
            range: range,
            withCharacters: text
        )

        let mask: Mask = pickMask(
            forText: updatedText,
            caretPosition: updatedText.index(updatedText.startIndex, offsetBy: field.caretPosition + text.count),
            autocomplete: autocomplete
        )

        let result: Mask.Result = mask.apply(
            toText: CaretString(
                string: updatedText,
                caretPosition: updatedText.index(updatedText.startIndex, offsetBy: field.caretPosition + text.count)
            ),
            autocomplete: autocomplete
        )

        field.allText = result.formattedText.string
        field.caretPosition = result.formattedText.string.distance(
            from: result.formattedText.string.startIndex,
            to: result.formattedText.caretPosition
        )

        return result
    }

    func pickMask(forText text: String, caretPosition: String.Index, autocomplete: Bool) -> Mask {
        let primaryAffinity: Int = calculateAffinity(
            ofMask: mask,
            forText: text,
            caretPosition: caretPosition,
            autocomplete: autocomplete
        )

        var masks: [(Mask, Int)] = affineFormats.map { (affineFormat: String) -> (Mask, Int) in
            let mask:     Mask = try! Mask.getOrCreate(withFormat: affineFormat, customNotations: customNotations)
            let affinity: Int  = calculateAffinity(
                ofMask: mask,
                forText: text,
                caretPosition: caretPosition,
                autocomplete: autocomplete
            )

            return (mask, affinity)
        }

        masks.sort { (left: (Mask, Int), right: (Mask, Int)) -> Bool in
            return left.1 > right.1
        }

        var insertIndex: Int = -1

        for (index, maskAffinity) in masks.enumerated() {
            if primaryAffinity >= maskAffinity.1 {
                insertIndex = index
                break
            }
        }

        if (insertIndex >= 0) {
            masks.insert((mask, primaryAffinity), at: insertIndex)
        } else {
            masks.append((mask, primaryAffinity))
        }

        return masks.first!.0
    }

    func calculateAffinity(
        ofMask mask: Mask,
        forText text: String,
        caretPosition: String.Index,
        autocomplete: Bool
    ) -> Int {
        return mask.apply(
            toText: CaretString(
                string: text,
                caretPosition: caretPosition
            ),
            autocomplete: autocomplete
        ).formattedText.string.prefixIntersection(with: text).count
    }

    func replaceCharacters(inText text: String, range: NSRange, withCharacters newText: String) -> String {
        if 0 < range.length {
            let result = NSMutableString(string: text)
            result.replaceCharacters(in: range, with: newText)
            return result as String
        } else {
            let result = NSMutableString(string: text)
            result.insert(newText, at: range.location)
            return result as String
        }
    }

}


extension String {

    func prefixIntersection(with string: String) -> Substring {
        let lhsStartIndex = startIndex
        var lhsEndIndex = startIndex
        let rhsStartIndex = string.startIndex
        var rhsEndIndex = string.startIndex

        while (self[lhsStartIndex...lhsEndIndex] == string[rhsStartIndex...rhsEndIndex]) {
            lhsEndIndex = lhsEndIndex != endIndex ? index(after: lhsEndIndex) : endIndex
            rhsEndIndex = rhsEndIndex != string.endIndex ? string.index(after: rhsEndIndex) : endIndex

            if (lhsEndIndex == endIndex || rhsEndIndex == string.endIndex) {
                return self[lhsStartIndex..<lhsEndIndex]
            }
        }

        return self[lhsStartIndex..<lhsEndIndex]
    }

}


extension UITextInput {
    var allText: String {
        get {
            guard let all: UITextRange = allTextRange
                else { return "" }
            return self.text(in: all) ?? ""
        }

        set(newText) {
            guard let all: UITextRange = allTextRange
                else { return }
            self.replace(all, withText: newText)
        }
    }

    var caretPosition: Int {
        get {
            if let responder = self as? UIResponder {
                // Workaround for non-optional `beginningOfDocument`, which could actually be nil if field doesn't have focus
                guard responder.isFirstResponder
                    else { return allText.count }
            }

            if let range: UITextRange = selectedTextRange {
                let selectedTextLocation: UITextPosition = range.start
                return offset(from: beginningOfDocument, to: selectedTextLocation)
            } else {
                return 0
            }
        }

        set(newPosition) {
            if let responder = self as? UIResponder {
                // Workaround for non-optional `beginningOfDocument`, which could actually be nil if field doesn't have focus
                guard responder.isFirstResponder
                    else { return }
            }

            if newPosition > allText.count {
                return
            }

            let from: UITextPosition = position(from: beginningOfDocument, offset: newPosition)!
            let to:   UITextPosition = position(from: from, offset: 0)!
            selectedTextRange = textRange(from: from, to: to)
        }
    }

    var allTextRange: UITextRange? {
        return self.textRange(from: self.beginningOfDocument, to: self.endOfDocument)
    }
}

ํ , ์ด์ œ ๋‚˜๋Š” ์ด๊ฒƒ์„ํ•ฉ๋‹ˆ๋‹ค :
<strong i="6">@IBOutlet</strong> var loginListener: PrefixAffinityMaskedTextFieldDelegate!

ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‚ด๊ฐ€ ์•Œ์•„ ์ฐจ๋ฆฐ ๊ฒƒ์€ Del ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋Œ€๊ด„ํ˜ธ ์•ˆ์˜ ์ˆซ์ž๋ฅผ ์‚ญ์ œํ•˜๋ฉด ์ˆซ์ž๊ฐ€ ์‚ญ์ œ๋˜๊ณ  ์ด์ œ ๋Œ€๊ด„ํ˜ธ ์•ˆ์˜ ์ˆซ์ž๊ฐ€ ํ•˜๋‚˜ ์ ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Backspace ๋ฒ„ํŠผ์œผ๋กœ ์‚ญ์ œํ•  ๋•Œ ์™ผ์ชฝ์—์„œ ์ˆซ์ž ๋˜๋Š” ์˜ค๋ฅธ์ชฝ์—์„œ ์ˆซ์ž๋ฅผ ๋ฐฐ์น˜/์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€๊ด„ํ˜ธ ์•ˆ์˜ ์ž๋ฆฟ์ˆ˜๋Š” ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋‚ด ๋ง์€:
์›๋ณธ: +7 (916) 000-00-00
del ํ‚ค ์‚ฌ์šฉ: +7 (96) 000-00-00
๋ฐฑ์ŠคํŽ˜์ด์Šค ํ‚ค ์‚ฌ์šฉ: 8 (796) 000-00-00

๋ณด์ •:
์ด์ œ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ ๊ฐ™์ง€๋งŒ textField์—์„œ ๋ฌธ์ž์—ด์„ ์‚ญ์ œํ•˜๋ ค๊ณ  ํ•  ๋•Œ '+' ๋ฌธ์ž๋งŒ ๋‚จ์•„ ์žˆ๊ณ  ๋‹ค์Œ์œผ๋กœ ์‚ญ์ œํ•˜๋ ค๊ณ  ํ•  ๋•Œ ์•ฑ์ด ์ถฉ๋Œํ•ฉ๋‹ˆ๋‹ค.

์Šค๋ ˆ๋“œ 1: ์น˜๋ช…์ ์ธ ์˜ค๋ฅ˜: endIndex ์ด์ƒ์œผ๋กœ ์ฆ๊ฐ€ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

@MrJox ๋‚ด๊ฐ€ ๋‹น์‹ ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ดํ•ดํ–ˆ๋Š”์ง€ ํ™•์‹ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
iOS์—๋Š” Del ํ‚ค๊ฐ€ ์—†๊ณ  $ Backspace ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€์ƒ ํ‚ค๋ณด๋“œ ์—๋ฎฌ๋ ˆ์ด์…˜์— ์†์ง€ ๋งˆ์‹ญ์‹œ์˜ค.

๋งˆ์ง€๋ง‰ ์˜ค๋ฅ˜์™€ ๊ด€๋ จํ•˜์—ฌ ๊ฒฝ์šฐ์— ๋”ฐ๋ผ ๋‹ค์Œ ํŒจ์น˜๋ฅผ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

extension String {

    func prefixIntersection(with string: String) -> Substring {
        guard !self.isEmpty && !string.isEmpty
        else { return "" }

        let lhsStartIndex = startIndex
        var lhsEndIndex = startIndex
        let rhsStartIndex = string.startIndex
        var rhsEndIndex = string.startIndex

        while (self[lhsStartIndex...lhsEndIndex] == string[rhsStartIndex...rhsEndIndex]) {
            lhsEndIndex = lhsEndIndex != endIndex ? index(after: lhsEndIndex) : endIndex
            rhsEndIndex = rhsEndIndex != string.endIndex ? string.index(after: rhsEndIndex) : endIndex

            if (lhsEndIndex == endIndex || rhsEndIndex == string.endIndex) {
                return self[lhsStartIndex..<lhsEndIndex]
            }
        }

        return self[lhsStartIndex..<lhsEndIndex]
    }

}

๋„ค ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

@MrJox ๋‹ค์Œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—…๋ฐ์ดํŠธ๋กœ ์ด ๋ฌธ์ œ๋ฅผ ์ข…๋ฃŒํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
๋Œ€์•ˆ ์ „๋žต์œผ๋กœ ์ด๊ฒƒ์„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์†Œ์Šค์— ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

@MrJox ์ตœ์ ํ™” ๋ฐ ๋‹จ์ˆœํ™” ๋ฒ„์ „:

extension String {

    func prefixIntersection(with string: String) -> Substring {
        var lhsIndex = startIndex
        var rhsIndex = string.startIndex

        while lhsIndex != endIndex && rhsIndex != string.endIndex {
            if self[...lhsIndex] == string[...rhsIndex] {
                lhsIndex = index(after: lhsIndex)
                rhsIndex = string.index(after: rhsIndex)
            } else {
                return self[..<lhsIndex]
            }
        }

        return self[..<lhsIndex]
    }

}

4.0.0 ์—๋Š” ์ด๊ฒƒ์„ ๊ธฐ๋Šฅ์œผ๋กœ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰