Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrong mask attached after editing #44

Closed
MrJox opened this issue Jul 25, 2018 · 11 comments
Closed

Wrong mask attached after editing #44

MrJox opened this issue Jul 25, 2018 · 11 comments
Assignees

Comments

@MrJox
Copy link

MrJox commented Jul 25, 2018

I have following masks set:
self.login.affineFormats = [ "{+7} ([000]) [000] [00] [00]", "{8} ([000]) [000]-[00]-[00]" ]

When i type a phone number with +7 mask and then edit the value within ([000]) mask switches to 8 instead of +7 and then 7 is put inside ([000])

@MrJox
Copy link
Author

MrJox commented Jul 25, 2018

Maybe there is a way to "lock" mask in textField once it was initialised with any of the masks?

@taflanidi
Copy link
Collaborator

Hi @MrJox, thanks for your report.
Could you please provide the exact states of the text inside your text field when this error occurs?

I'll be able to trace it later today and then help you with the solution.

@MrJox
Copy link
Author

MrJox commented Jul 25, 2018

Not absolutely sure what you mean by states but here's what happens:

I have a phone number read from cache and inserted into textField (It's a custom JVFloat textField).
The phone number is +7 (926) 000-00-00 and then I set the pointer after '2' and delete this number. As a result my textView content converts into 8 (796) 000-00-00.

So for some reason it deletes + and treats 7 not as mask but as an actual part of phone number and puts it into brackets and then adds the 8 mask.

@taflanidi
Copy link
Collaborator

Yeah, your description works for me, thanks!
Stay tuned.

@taflanidi
Copy link
Collaborator

@MrJox while I'm working on some improvements and fixes required by our last library update, I've put together a quick solution for your problem.

Effectively, it's a PolyMaskTextFieldDelegate modification with a custom affinity calculation method. Instead of an overall text affinity with the mask, this method compares the length of the prefixes intersection, the count of characters.

This code or its modified version might find its way to the library sources eventually.

import Foundation
import UIKit

import InputMask

@IBDesignable
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)
    }
}

@MrJox
Copy link
Author

MrJox commented Jul 26, 2018

Hm, so now i do this:
@IBOutlet var loginListener: PrefixAffinityMaskedTextFieldDelegate!

But it still won't work. What i noticed, if you delete digits within brackets with Del key, the digit gets deleted and now it's one less digit within brackets however when I delete with Backspace button, it places/moves either the digit from the left or digit from the right so the amount of digits inside brackets stay unchanged.

What I mean:
original: +7 (916) 000-00-00
with del key: +7 (96) 000-00-00
with backspace key: 8 (796) 000-00-00

Correction:
Now it seems to work however when I try to delete the string in the textField, I have only '+' character remaining and then my app crashes when i try to delete it with

Thread 1: Fatal error: cannot increment beyond endIndex

@taflanidi
Copy link
Collaborator

@MrJox I'm not sure I understood you correctly.
Be noticed, there's no Del key on iOS, only Backspace. Don't let virtual keyboard emulation fool you.

Regarding your last error, you may consider following patch for the edge case:

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
Copy link
Author

MrJox commented Jul 26, 2018

Yeah it fixed it, thanks!

@taflanidi
Copy link
Collaborator

@MrJox I'll close this issue with the next library update.
I'm considering adding this to the library sources as an alternative strategy.

@taflanidi
Copy link
Collaborator

@MrJox optimized & simplified version:

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]
    }
    
}

@taflanidi
Copy link
Collaborator

4.0.0 contains this as a feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants