質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

91.02%

  • Swift

    6087questions

    Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

textViewにおいてボタンをタップしたらカーソルの左の文字を1字削除するコード

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 203

Tomzy

score 65

やりたいこと

textViewにおいてボタンをタップしたらカーソルの左の文字を1字削除することです。

やったこと

ボタンをタップしたらカーソルが1つ左に戻ることは下記のコードで成功しました。

    @IBAction func arrow03(_ sender: Any) {

// To one position to the left of the current cursor position

        // only if there is a currently selected range
        if let selectedRange = InputView.selectedTextRange {

            // and only if the new position is valid
            if let newPosition = InputView.position(from: selectedRange.start, offset: -1) {

                // set the new position
                InputView.selectedTextRange = InputView.textRange(from: newPosition, to: newPosition)
       print("ここまで来た05")     }
        }
    }

そこで下記のコードで試しましたところ、後述のエラーがでて行き詰まりました。

        @IBAction func Sakujo01(_ sender: Any) {
            // only if there is a currently selected range
            if let selectedRange = InputView.selectedTextRange {

                // and only if the new position is valid
                if let newPosition = InputView.position(from: selectedRange.start, offset: -1) {

                    // ←削除ボタンが押された時の処理
                    InputView.selectedText = ""

出たエラー

Value of type 'UITextView' has no member 'selectedText'


エラーの場所
イメージ説明

お願い

上記のエラーを解決してボタンをタップしたらカーソルの左の文字を1字削除する方法を教えてください。

ご参考事項

  • 環境は下記のとおりです。
    MacBook Pro (15-inch, 2016)
    High Sierra OS10.13
    Version 9.1 (9B55)Swift4にバージョンアップしました。

  • 下記のように定義しています。
    @IBOutlet weak var InputView: UITextView!
    @IBOutlet weak var Sentaku01: UIButton!

  • 画面は下記のとおりです。最初はキーボードはでておらず画面をタップして文字記入後削除ボタンを押して文字を消したいと思っております。
    イメージ説明
    イメージ説明

以上よろしくお願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

エラーの原因はUITextViewにselectedTextという変数がないことです。UITextViewを覗いてみたところ確かにselectedTextは宣言されてませんでした(selectedRangeはあったんですが)。そこでコードを再度拝見したところ何らかのrangeは取得されているようなので、目先を少し変えて文字列の要らないところを除外しつつ二つに分割、その後再度結合するというやり方はいかがでしょうか。このやり方なら

textViewにおいてボタンをタップしたらカーソルの左の文字を1字削除する

という条件を満たせるかと存じます。

サンプルコード

let alphabet = "abcdefghijklmn"
let startIndex = alphabet.startIndex
let fIndex = alphabet.index(startIndex, offsetBy: 5)
let gIndex = alphabet.index(after: fIndex)
let endIndex = alphabet.endIndex
let firstStrings = alphabet[startIndex..<fIndex]
let secondStrings = alphabet[gIndex..<endIndex]
let dropF = firstStrings + secondStrings
print(dropF). //abcdeghijklmn

以下追記

ネットやUITextViewのコードを眺めてたらUITextViewのrangeやpositionって扱いにくいですねこれ。なのでもう一つ思いつきました。

@IBAction func deleteText(_ sender: UIButton) {
    if let selectedRange = textView.selectedTextRange {
        textView.replace(selectedRange, withText: "")
    }
}

これならどうでしょうか?

追記の追記

@IBAction func deleteCharacterAtLeftOfCaret(_ sender: UIButton) {

        //キャレットの位置
     //文字列が"あいうえお"の時、キャレットが"あ"の左側にあるならばoffset == 0
        let offset = textView.selectedRange.location
     
     //キャレットが一番左にある場合削除する文字がないので抜ける
        if offset <= 0 {
            return
        }

     //テキストがない場合抜ける
        guard let fullText = textView.text else {
            return
        }

        let startIndex = fullText.startIndex
        let endIndex = fullText.endIndex

        //UITextView.index(_,offsetBy:)で得られるindexが負の数になると落ちる
        let targetOfLeftIndex = fullText.index(startIndex, offsetBy: offset - 1)
        let targetOfRightIndex = fullText.index(startIndex, offsetBy: offset)

        let leftText = fullText[startIndex..<targetOfLeftIndex]
        let rightText = fullText[targetOfRightIndex..<endIndex]

        let modifiedText = leftText + rightText

        textView.text = String(modifiedText)

    }

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/11/03 14:58

    ありがとうございました。
    私が狙っている削除をする文字列は1字の時もあるし100字の時もあります。頂いたコードをPlayGroundに下記入れて、いろいろ考えました。その結果offsetByを0にするとfirstStringsが""になり削除されることがわかりました。
    以下のコードをどう私のアプリに折り込むかが問題ですが、これをベースに考えてみます。

    let alphabet = "abcd"
    let startIndex = alphabet.startIndex
    let fIndex = alphabet.index(startIndex, offsetBy: 0)
    let firstStrings = alphabet[startIndex..<fIndex]
    let dropF = firstStrings
    print(dropF) //abcd

    キャンセル

  • 2017/11/03 16:08 編集

    定数alphabetは"abcd"でありこれは["a", "b", "c", "d"]の配列です。定数startIndexは0番目、定数fIndexも0番目になります。alphabet[startIndex..<fIndex]というのはalphabet[0..<0]、つまり空の配列[]が返ってるだけです。なのでこの場合print(dropF)は何も表示されません。
    削除というニュアンスとは少々違うかもしれないです。

    キャンセル

  • 2017/11/03 16:09

    早速下記のとおり実装してみました。
    ビルドは成功し、あかさたな とキーボードでインプットし、←削除ボタンを押しても反応がありません。ここまで来た05 は記録されます。

    //←削除を押した時⇒左の文字を削除
    @IBAction func Sakujo01(_ sender: Any) {


    if let selectedRange = InputView.selectedTextRange {
    InputView.replace(selectedRange, withText: "")
    print("ここまで来た05") }
    }

    キャンセル

  • 2017/11/03 16:24

    わかりました。あかさたな とインプットした上で文字 さた を選択した上で←削除ボタンをタップしたら、見事に さた 文字だけが削除されました。

    このボタンは選択しないで単にカーソルの左を削除するものなのです(キーボードの削除ボタンと同じ機能)。よろしくお願いします。

    キャンセル

  • 2017/11/03 16:27

    ←選択 ボタンもまだ実装できていません。それが分かれば、そのコードを記載した上でこのコードで削除することができるわけですね。

    キャンセル

  • 2017/11/03 17:04

    >このボタンは選択しないで単にカーソルの左を削除するものなのです。
    これに関しては自分の理解力の低さを改めて感じました。俺文章もっと読めよと。その理解力の低さにお付き合いさせてしまってすいませんでした。

    ←選択ボタンに関してはselectedRangeとselectedTextRangeと現状あるコードをこねくり回せばなんとかなりそうなところまですでにいらっしゃるんじゃないかと思いますよ。

    キャンセル

  • 2017/11/03 17:08

    成功しました。ありがとうございました。

    下記のとおり実装しましたら、思った通りの機能が搭載できました。
    キャレットが文字列の先頭に行かず削除したところにとどまっています。
    あと、選択やコピーペーストが残っていますが、今回教わったコードをよく勉強してやってみます。ありがとうございました。

    //←削除を押した時⇒左の文字を削除
    @IBAction func Sakujo01(_ sender: Any) {


    if let fullText = InputView.text {
    let startIndex = fullText.startIndex
    let endIndex = fullText.endIndex
    let offset = InputView.selectedRange.location
    let targetOfLeftIndex = fullText.index(startIndex, offsetBy: offset - 1)
    let targetOfRightIndex = fullText.index(startIndex, offsetBy: offset)
    let leftText = fullText[startIndex..<targetOfLeftIndex]
    let rightText = fullText[targetOfRightIndex..<endIndex]
    let modifiedText = leftText + rightText
    InputView.text = String(modifiedText)
    }
    }

    キャンセル

  • 2017/11/03 17:14

    キャレットの位置が留まってるのは良かったです。arrow03関数内のコードの影響かもしれないですね。
    私も新たな知見を得る事が出来ました。こちらこそありがとうございました。

    キャンセル

  • 2017/11/03 17:42

    いやー、それにしても短時間に、これだけのコードを書いてしまうなんて凄いですね。勉強の意欲がでます。それにしても、今回の課題はアップルさんの標準の仕様(画面をタップして文字を選択するなど)から外れてボタンでいろいろな操作を行うものですからウェブサイト上にもサンプルが少なく本当に困りました。

    私は年寄りのためのアプリを開発している82歳のクリエイターです。アップルさんには分からない年寄りの悩みを解決するためにやっています。ご参考までに、年寄りの指では小さな文字を選択したりカーソルを動かすことは難しいのです。今後ともご指導ください。

    キャンセル

  • 2017/11/04 09:28

    愚見ではありますが少数派の人々に対して関与していくというのは幅広く意義のある事だと感じています。ウチの母もiPhoneを使っていますが小さい文字を読む、テキストを操作するのは難しいと話していました。また父もiPhoneに興味を持ってはいるのですが母と同じくテキスト操作が難しいという話をしていました(PCでの操作も難しく感じているようです)。ですのでTomzyさんのアプリが完成したら是非とも両親に紹介したいです。

    実は回答前にプロフィールを拝見してましてとてもクールなクリエイターの方だなと思っていました。微力ではありますがTomzyさんのお力になれたら嬉しく思います。

    キャンセル

  • 2017/11/04 11:58

    ありがとうこざいます。
    すでに年寄り用の スマホの勉強 をいくつかリリースしています。若い応援者のために設定を整理し年寄りのためには動画で操作方法を説明しています。どうぞご利用ください。

    ところで教えていただいたカーソルの手前を削除するボタン操作ですが、カーソルが最初に戻った状態ではボタンを操作するとアプリが落ちます。その対策としてイフ文を使いカーソルが最初の場合は何にもしないと言うコードを考えています。

    キャンセル

  • 2017/11/04 12:55 編集

    「スマホの勉強」に興味を惹かれました。両親に伝えてみます。

    コードに関してですが確かに落ちてしまいますね。
    理由としてはUITextView.Index(_:offsetBy:)の二つ目の引数が負の値になるのがマズイようです。
    なので対策としてはTomzyさんの考えてらっしゃるアプローチでいけると思います。
    それと若干コードの可読性をあげてみました。

    キャンセル

  • 2017/11/05 11:09

    まずは、自分でコードを考えようとしました。guareも使えました。
    //←削除を押した時⇒左の文字を削除
    @IBAction func Sakujo01(_ sender: Any) {
    let characters = String.characters
    guard
    InputView [characters.startIndex] = ""
    else {
    //以下は最初と一緒
    if let fullText = InputView.text {
    let startIndex = fullText.startIndex
    let endIndex = fullText.endIndex
    let offset = InputView.selectedRange.location
    let targetOfLeftIndex = fullText.index(startIndex, offsetBy: offset - 1)
    let targetOfRightIndex = fullText.index(startIndex, offsetBy: offset)
    let leftText = fullText[startIndex..<targetOfLeftIndex]
    let rightText = fullText[targetOfRightIndex..<endIndex]
    let modifiedText = leftText + rightText
    InputView.text = String(modifiedText)
    }}}}

    しかしながら、下記のエラーがでてしまいました。
    Instance member 'characters' cannot be used on type 'String'

    そこで、教えて頂いた
    let offset = textView.selectedRange.location
         //テキストがない場合抜ける
    guard let fullText = textView.text else {
    return
    }
    を使って下記のコードにしました(FIXの指示がでてFIX実施結果)。ビルドも成功し、カーソルの左側は削除さます。しかし、最初にカーソルが位置する時に削除ボタンを押すと落ちて固まります。原因を考えているところです。

    //←削除を押した時⇒左の文字を削除
    @IBAction func Sakujo01(_ sender: Any) {

    _ = InputView.selectedRange.location

    //テキストがない場合抜ける
    guard (InputView.text) != nil else {
    return
    }
    if let fullText = InputView.text {
    let startIndex = fullText.startIndex
    let endIndex = fullText.endIndex
    let offset = InputView.selectedRange.location
    let targetOfLeftIndex = fullText.index(startIndex, offsetBy: offset - 1)
    let targetOfRightIndex = fullText.index(startIndex, offsetBy: offset)

    let leftText = fullText[startIndex..<targetOfLeftIndex]
    let rightText = fullText[targetOfRightIndex..<endIndex]

    let modifiedText = leftText + rightText

    InputView.text = String(modifiedText)
    }
    }

    キャンセル

  • 2017/11/05 12:02

    下記のとおり修正しましたら狙ったとおりになりました。
    ( guard (InputView.text) != nil を ="" に修正しました }

    //←削除を押した時⇒左の文字を削除
    @IBAction func Sakujo01(_ sender: Any) {

    _ = InputView.selectedRange.location

    //テキストがない場合抜ける
    guard (InputView.text) != "" else {
    return
    }
    if let fullText = InputView.text {
    let startIndex = fullText.startIndex
    let endIndex = fullText.endIndex
    let offset = InputView.selectedRange.location
    let targetOfLeftIndex = fullText.index(startIndex, offsetBy: offset - 1)
    let targetOfRightIndex = fullText.index(startIndex, offsetBy: offset)

    let leftText = fullText[startIndex..<targetOfLeftIndex]
    let rightText = fullText[targetOfRightIndex..<endIndex]

    let modifiedText = leftText + rightText

    InputView.text = String(modifiedText)
    }
    }

    キャンセル

  • 2017/11/05 13:33

    そのコードだと自分の方だと落ちちゃいますね。

    >イフ文を使いカーソルが最初の場合は何にもしないと言うコード

    というのを考えてらっしゃったようでしたので回答にはその辺りを記入していなかったのですがそのあたりも含めて回答修正します。

    キャンセル

  • 2017/11/05 16:53

    ありがとうございました。
    大分コードの意味が分かってきました。
    結局下記のとおりにして上手く動きました。
    これから選択やコピーペーストに挑戦します。
    行き詰まったらまた質問します。
    よろしくお願いします。
    最終コード
     ↓ ↓ ↓
    //←削除を押した時⇒左の文字を削除
    @IBAction func Sakujo01(_ sender: Any) {
    //キャレットの位置
    //文字列が"あいうえお"の時、キャレットが"あ"の左側にあるならばoffset == 0
    let offset = InputView.selectedRange.location

    //キャレットが一番左にある場合削除する文字がないので抜ける
    if offset <= 0 {
    return
    }
    //テキストがない場合抜ける
    guard let fullText = InputView.text else {
    return
    }

    let startIndex = fullText.startIndex
    let endIndex = fullText.endIndex

    //UITextView.index(_,offsetBy:)のoffsetByは負の数だと落ちる
    let targetOfLeftIndex = fullText.index(startIndex, offsetBy: offset - 1)
    let targetOfRightIndex = fullText.index(startIndex, offsetBy: offset)

    let leftText = fullText[startIndex..<targetOfLeftIndex]
    let rightText = fullText[targetOfRightIndex..<endIndex]

    let modifiedText = leftText + rightText

    InputView.text = String(modifiedText)

    }

    キャンセル

  • 2017/11/06 09:53

    無事動いてよかったです。選択やペーストの実装も上手くいくといいですね。お答えできる質問でしたらいつでも歓迎です。

    キャンセル

  • 2017/11/06 10:34

    ありがとうございました。
    カーソルの右を削除する問題にチャレンジして、少し前に動きました。一部機能するのですが不具合もでました。別途質問を掲載しますのでよろしくお願いします。

    キャンセル

  • 2017/11/08 12:06

    削除後にカーソルの位置を削除した文字のところに持ってくるように、下記のコードを入れました。

    //削除後の表示
    InputView.text = String(modifiedText)

    //カーソルを削除した文字の後につける

    InputView.selectedRange.location = (offset-1)

    キャンセル

  • 2017/11/16 16:52

    絵文字を削除するとアプリが落ちる不具合の解決

    その後、この不具合を解決するためにxAxisさんの助言を受けて下記のコードで解決しました。

    下記のコード内にアプローチについても整理しました。
    ただ、絵文字を削除する場合2とか3回ボタンを押す必要があります。右の文字を削除する場合は一発でした。

    コード
     ↓ ↓ ↓
    //←削除を押した時⇒左の文字を削除
    /*
    アプローチについてカーソルの左文字削除について整理

    絵文字を考慮しつつカーソルの左側を削除する場合、考えることは
    1.入力された全ての文字列
    2.startIndexからカーソルまでの文字列
    3.削除する文字数
    4.カーソルの左側に残った文字列の数
    5.UTF16の文字列は「あ」という文字は1文字として認識されるが「😓」という文字は2文字として認識される。さらに色付きの文字だったりすると倍になったりする(詳細はUTF16, unicode等検索して下さい)
    6.5番を踏まえるとカーソルの左が普通の文字であろうと絵文字であろうと対応できるコードを書かなければならない
    7. カーソルの左側を削除する場合もう一つ(というかこっちの方がはるかに楽な)アプローチがあります。それはUITextView.deleteBackward()を利用する方法です。こっちの方が楽に削除出来るかと。

    1.はfullText
    2.はfullText[startIndex..<cursorIndex]
    3.はカーソルの左側1文字だけなのでamountOfCharacter = 1
    4.は実機等画面に表示されている全ての文字列の数 - rightTextの数 - ammountOfCharacter
    で表します。
    5.これのおかげでUITextView.index(_,offsetBy:)で隣の文字を取得するという方法が使えません。
    6.なので4番で取得した文字列分fullTextから切り取る、という方法を取ります。
    7.カーソルの左の1文字を削除する ボタンActionに」より⇒を作動させる InputView.deleteBackward()
    */


    @IBAction func Sakujo01(_ sender: Any) {
    print("ここまで来た08b")

    //guard は条件を満たしていない時に、迅速にスコープを抜けるための処理(breakやreturn、例外のthrowなど)を行う。
    //テキストがない場合抜ける

    guard let fullText = InputView.text else {
    return

    }
    print("ここまで来た08c")

    //カーソルの位置を定義
    let offset = InputView.selectedRange.location

    //文字列が"あいうえお"の時、キャレットが"あ"の左側にあるならばoffset == 0
    //キャレットが一番左にある場合削除する文字がないので抜ける
    if offset <= 0 {
    return
    }
    print("ここまで来た08d")
    //カーソルの左の文字列
    let startIndex = fullText.utf16.startIndex
    let endIndex = fullText.utf16.endIndex

    //1文字のスペース
    let amountOfCharacter = 1
    //カーソルの左の1文字を削除する
    InputView.deleteBackward()
    print("ここまで来た08e")

    //カーソルの左より1文字削除後の左の文字列
    let targetOfLeftIndex = fullText.utf16.index(startIndex, offsetBy: offset - amountOfCharacter)
    //カーソルの右側の文字列
    let targetOfRightIndex = fullText.utf16.index(startIndex, offsetBy: offset)

    //左の削除後の文字列定義

    let leftText = fullText[startIndex..<targetOfLeftIndex]

    let rightText = fullText[targetOfRightIndex..<endIndex]

    let modifiedText = leftText + rightText
    //左の削除後の文字列の表示
    InputView.text = String(modifiedText)

    //カーソルの位置表示
    InputView.selectedRange.location = offset-amountOfCharacter
    }

    キャンセル

  • 2017/11/16 20:08

    左側の文字の削除は

    @IBAction func tap(_ sender: UIBarButtonItem) {
    textView.deleteBackward()
    }

    これでもいいです。

    キャンセル

  • 2017/11/16 20:10

    ちなみにdeleteForward()は無いので実装しました。今までやってきた事がdeleteForward()とでも言えるような内容です。まだ詰めなければなりませんが。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 91.02%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • Swift

    6087questions

    Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています