web-dev-qa-db-ja.com

カーソルを最後に移動せずにUITextFieldテキストをフォーマットする

NSAttributedStringスタイルをITextFieldに適用しようとしています。新しいテキストエントリを処理した後、キーストロークでキーストロークします。問題は、テキストを置き換えるたびに、変更のたびにカーソルが最後までジャンプすることです。これが私の現在のアプローチです…

テキストの変更をすぐに受信して表示します(Iがfalseを返す場合に同じ問題が適用され、テキストを手動で置換します)

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool {        
    let range:NSRange = NSRange(location: range.location, length: range.length)
    let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string);
    return true
}

UITextFieldTextDidChangeNotification通知をサブスクライブしました。これはスタイリングをトリガーします。テキストが変更されたら、format()関数を使用して、フォーマットされたバージョン(NSAttributedString)でテキスト(NSString)を置き換えます。

func textFieldTextDidChangeNotification(userInfo:NSDictionary) {
    var attributedString:NSMutableAttributedString = NSMutableAttributedString(string: fullString)
    attributedString = format(textField.text) as NSMutableAttributedString
    textField.attributedText = attributedString
}

スタイリングはこのようにうまく機能します。ただし、テキストを置き換えるたびに、カーソルは文字列の末尾にジャンプします。この動作をオフにしたり、編集を開始した位置にカーソルを手動で戻したりするにはどうすればよいですか? …または編集中にテキストのスタイルを設定するより良い解決策はありますか?

28
Bernd

これを機能させるには、カーソルの位置を取得し、フィールドの内容を更新してから、カーソルを元の位置に戻します。 Swiftでの正確な同等機能はわかりませんが、Obj-Cでこれを行う方法を次に示します。

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    UITextPosition *beginning = textField.beginningOfDocument;
    UITextPosition *cursorLocation = [textField positionFromPosition:beginning offset:(range.location + string.length)];

    textField.text = [textField.text stringByReplacingCharactersInRange:range withString:string];

    /* MAKE YOUR CHANGES TO THE FIELD CONTENTS AS NEEDED HERE */

    // cursorLocation will be (null) if you're inputting text at the end of the string
    // if already at the end, no need to change location as it will default to end anyway
    if(cursorLocation)
    {
        // set start/end location to same spot so that nothing is highlighted
        [textField setSelectedTextRange:[textField textRangeFromPosition:cursorLocation toPosition:cursorLocation]];
    }

    return NO;
}
38
Stonz2

Objective-Cのコードで@ Stonz2に感謝します。それは魅力のように機能します!私はSwiftプロジェクトで使用しました。Swiftで同じコードを使用します:

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

    let positionOriginal = textField.beginningOfDocument
    let cursorLocation = textField.positionFromPosition(positionOriginal, offset: (range.location + NSString(string: string).length))

    /* MAKE YOUR CHANGES TO THE FIELD CONTENTS AS NEEDED HERE */

    if(cursorLocation != nil) {
        textField.selectedTextRange = textField.textRangeFromPosition(cursorLocation!, toPosition: cursorLocation!)
    }
    return false 
}
9
pcsantana

これは古い質問ですが、同様の問題があり、次のSwiftで解決しました。

// store the current cursor position as a range
var preAttributedRange: NSRange = textField.selectedRange 
// apply attributed string
var attributedString:NSMutableAttributedString = NSMutableAttributedString(string: fullString)
attributedString = format(textField.text) as NSMutableAttributedString
textField.attributedText = attributedString
// reapply the range
textField.selectedRange = preAttributedRange

それは私のアプリのコンテキストで動作します。うまくいけば、誰かにとって便利です!

9
jolyonruss

このスレッドの他の正しい答えを補足するために、ここにSwift 4.2およびITextFieldITextViewの両方のコードスニペットをいくつか示します。

ITextField

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    // Update Cursor
    let positionOriginal = textField.beginningOfDocument
    let cursorLocation = textField.position(from: positionOriginal, offset: (range.location + string.count))
    if let cursorLocation = cursorLocation {
        textField.selectedTextRange = textField.textRange(from: cursorLocation, to: cursorLocation)
    }
    return false
}

ITextView

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    // Update Cursor
    let positionOriginal = textView.beginningOfDocument
    let cursorLocation = textView.position(from: positionOriginal, offset: (range.location + text.count))
    if let cursorLocation = cursorLocation {
        textView.selectedTextRange = textView.textRange(from: cursorLocation, to: cursorLocation)
    }
    return false
}
3
anders

これはSwift 2.で動作するコードです。お楽しみください!

 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let beginnig: UITextPosition =  textField.beginningOfDocument

    if let txt = textField.text {
        textField.text = NSString(string: txt).stringByReplacingCharactersInRange(range, withString: string)
    }

    if let cursorLocation: UITextPosition = textField.positionFromPosition(beginnig, offset: (range.location + string.characters.count) ) {
        textField.selectedTextRange = textField.textRangeFromPosition(cursorLocation, toPosition: cursorLocation)
    }

    return false
}

最後の「if let」は常にコードの最後に留まる必要があることに注意してください。

2
DuffMck

ここで別の質問への回答を便乗させるには https://stackoverflow.com/a/51814368/431271shouldChangeCharactersInRangeのテキストを変更しないでくださいデリゲートメソッドは、変更を許可するかどうかをフィールドに通知することのみを目的としており、変更することは想定されていません。

代わりに、次のように、値の変更をサブスクライブしてテキストの変更を処理します。

textField.addTarget(self, action: #selector(handleTextChanged), for: .editingChanged)

// elsewhere in the file
func handleTextChanged(_ textField: UITextField) {
    textField.sanitizeText(map: formatText)
}

sanitizeTextの実装は次のようになります。

extension UITextField {

    // Use this to filter out or change the text value without 
    // losing the current selection
    func sanitizeText(map: ((String) -> String)) {
        guard let text = self.text,
            let selection = selectedTextRange else {
                return
        }

        let newText = map(text)

        // only execute below if text is different
        guard newText != text else { return }

        // determine where new cursor position should start
        // so the cursor doesnt get sent to the end
        let diff = text.count - newText.count
        let cursorPosition = offset(from: beginningOfDocument, to: selection.start) - diff

        self.text = newText

        // notify the value changed (to ensure delegate methods get triggered)
        sendActions(for: .valueChanged)

        // update selection afterwards
        if let newPosition = position(from: beginningOfDocument, offset: cursorPosition) {
            selectedTextRange = textRange(from: newPosition, to: newPosition)
        }
    }
}
0
ccwasden