Giter VIP home page Giter VIP logo

Comments (11)

MrJox avatar MrJox commented on September 28, 2024 2

Yeah it fixed it, thanks!

from input-mask-ios.

taflanidi avatar taflanidi commented on September 28, 2024 1

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

from input-mask-ios.

taflanidi avatar taflanidi commented on September 28, 2024 1

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

from input-mask-ios.

taflanidi avatar taflanidi commented on September 28, 2024 1

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

from input-mask-ios.

MrJox avatar MrJox commented on September 28, 2024

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

from input-mask-ios.

taflanidi avatar taflanidi commented on September 28, 2024

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.

from input-mask-ios.

MrJox avatar MrJox commented on September 28, 2024

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.

from input-mask-ios.

taflanidi avatar taflanidi commented on September 28, 2024

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

from input-mask-ios.

MrJox avatar MrJox commented on September 28, 2024

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

from input-mask-ios.

taflanidi avatar taflanidi commented on September 28, 2024

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

from input-mask-ios.

taflanidi avatar taflanidi commented on September 28, 2024

4.0.0 contains this as a feature.

from input-mask-ios.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.