62219


Xcode11.3  
Swift5  
macOSMojave10.14.6

Facebookのコメント投稿画面のような、タイムライン画面（TableView）から記事（セル）をタップし、遷移先画面でコメントを投稿できる機能を実装したのですが、コメント投稿を試みる際（teXtFieldをタップした時）、時折`Thread
1: EXC_BREAKPOINT (code=1,
subcode=0x104190304)`のエラーが発生します。発生する時と発生しない時があります。発生箇所はテキストフィールドがキーボードと被らないようにする部分です（画像右側）。コンソールにはFinal
error、reason等の表示はありません。  
原因と対策をご教授願います。

コメント表示部分はtableViewを使用し、tableViewCellを使用しています。  
[![画像の説明をここに入力](https://i.stack.imgur.com/YaE3Z.jpg)](https://i.stack.imgur.com/YaE3Z.jpg)

●タイムライン画面から遷移したコメント投稿画面

[code]

    import UIKit
    import Firebase
    import SVProgressHUD
    
    class ArticleViewController: UIViewController,UITextFieldDelegate, UITableViewDataSource, UITableViewDelegate {
    
        @IBOutlet weak var articleImage: UIImageView!
        @IBOutlet weak var articleLabel: UILabel!
        @IBOutlet weak var articleButton: UIButton!
        @IBOutlet weak var articleLilkeCount: UILabel!
        @IBOutlet weak var commentTextField: UITextField!
        @IBOutlet weak var commentTableView: UITableView!
    
        var postArray: [PostData] = []
        //textFieldが動くようにする
        private var activeTextField: UITextField?
    
        //前画面からデータを受け取るための変数
        var postDataReceived: PostData?
    
        // DatabaseのobserveEventの登録状態を表す
        var observing = false
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            commentTableView.delegate = self
            commentTableView.dataSource = self
            // テーブルセルのタップを無効にする
            commentTableView.allowsSelection = false
    
            guard let postData = postDataReceived else {
                return
            }
            let nib = UINib(nibName: "CommentTableViewCell", bundle: nil)
            commentTableView.register(nib, forCellReuseIdentifier: "Cell2")
    
            // テーブル行の高さをAutoLayoutで自動調整する
            commentTableView.rowHeight = UITableView.automaticDimension
            // テーブル行の高さの概算値を設定しておく
            // 高さ概算値 = 「縦横比1:1のUIImageViewの高さ(=画面幅)」+「いいねボタン、キャプションラベル、その他余白の高さの合計概算(=100pt)」
            commentTableView.estimatedRowHeight = UIScreen.main.bounds.width
    
            //イメージ画像
            self.articleImage.image = postData.image
            //キャプションのテキスト
            self.articleLabel.text = "\(postData.caption!)"
            //いいねの数
            let likeNumber = postData.likes.count
            articleLilkeCount.text = "\(likeNumber)"
    
    
            if Auth.auth().currentUser != nil {
                if self.observing == false {
                    // 要素が追加されたらpostArrayに追加してTableViewを再表示する
                    let postsRef = Database.database().reference().child(Const.PostPath)
                    postsRef.observe(.childAdded, with: { snapshot in
                        print("DEBUG_PRINT: .childAddedイベントが発生しました。")
    
                        // PostDataクラスを生成して受け取ったデータを設定する
                        if let uid = Auth.auth().currentUser?.uid {
                            let postData = PostData(snapshot: snapshot, myId: uid)
                            self.postArray.insert(postData, at: 0)
    
                            // TableViewを再表示する
                            self.commentTableView.reloadData()
                        }
                    })
                    // 要素が変更されたら該当のデータをpostArrayから一度削除した後に新しいデータを追加してTableViewを再表示する
                    postsRef.observe(.childChanged, with: { snapshot in
                        print("DEBUG_PRINT: .childChangedイベントが発生しました。")
    
                        if let uid = Auth.auth().currentUser?.uid {
                            // PostDataクラスを生成して受け取ったデータを設定する
                            let postData = PostData(snapshot: snapshot, myId: uid)
    
                            // 保持している配列からidが同じものを探す
                            var index: Int = 0
                            for post in self.postArray {
                                if post.id == postData.id {
                                    index = self.postArray.firstIndex(of: post)!
                                    break
                                }
                            }
    
                            // 差し替えるため一度削除する
                            self.postArray.remove(at: index)
    
                            // 削除したところに更新済みのデータを追加する
                            self.postArray.insert(postData, at: index)
    
                            // TableViewを再表示する
                            self.commentTableView.reloadData()
                        }
                    })
    
                    // DatabaseのobserveEventが上記コードにより登録されたため
                    // trueとする
                    observing = true
                }
            } else {
                if observing == true {
                    // ログアウトを検出したら、一旦テーブルをクリアしてオブザーバーを削除する。
                    // テーブルをクリアする
                    postArray = []
                    commentTableView.reloadData()
                    // オブザーバーを削除する
                    let postsRef = Database.database().reference().child(Const.PostPath)
                    postsRef.removeAllObservers()
    
                    // DatabaseのobserveEventが上記コードにより解除されたため
                    // falseとする
                    observing = false
                }
            }
    
            //textFieldがキーボードで隠れないようにする
            self.commentTextField.delegate = self
            self.setUpNotificationForTextField()
        }
        func setPostData(_ postData: PostData) {
            postDataReceived = postData
        }
        //textFieldがキーボードで隠れないようにする
        internal func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
            self.activeTextField = textField
            return true
        }
        //textFieldがキーボードで隠れないようにする
         internal func setUpNotificationForTextField() {
               let notificationCenter = NotificationCenter.default
               notificationCenter.addObserver(self, selector: #selector(self.handleKeyboardWillShowNotification(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
               notificationCenter.addObserver(self, selector: #selector(self.handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
           }
        //textFieldがキーボードで隠れないようにする
        @objc private func handleKeyboardWillShowNotification(_ notification: Notification) {
            let userInfo = notification.userInfo //この中にキーボードの情報がある
            let keyboardSize = (userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
            let keyboardY = self.view.frame.size.height - keyboardSize.height //画面全体の高さ - キーボードの高さ = キーボードが被らない高さ
            let editingTextFieldY: CGFloat = (self.activeTextField?.frame.origin.y)!
            if editingTextFieldY > keyboardY - 60 {
                UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseIn, animations: {
                    self.view.frame = CGRect(x: 0, y: self.view.frame.origin.y - (editingTextFieldY - (keyboardY - 60)), width: self.view.bounds.width, height: self.view.bounds.height)
                }, completion: nil)
    
            }
        }
        //textFieldがキーボードで隠れないようにする
        @objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
            UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseIn, animations: {
                self.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
            }, completion: nil)
        }
        //コメントテーブルビュー
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
               return postArray.count
           }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
           // セルを取得してデータを設定する
           let cell2 = tableView.dequeueReusableCell(withIdentifier: "Cell2", for: indexPath) as! CommentTableViewCell
           cell2.commentSetPostData(postArray[indexPath.row])
    
           // セル内のボタンのアクションをソースコードで設定する
           cell2.goodButton.addTarget(self, action:#selector(handleGoodButton(_:forEvent:)), for: .touchUpInside)
    
           return cell2
        }
        //コメント投稿ボタン
        @IBAction func commentButton(_ sender: UIButton) {
            // 前画面から受け取ったデータを取り出す
            guard let postData = postDataReceived else {
                return
            }
    
            //cell.textField.textがnilじゃなかったら、commentTextとする
            if let commentText = self.commentTextField.text  {
    
                //であれば、cell(PostTableViewCell)のtextFieldをプリントする
                print(self.commentTextField.text as Any)
    
                //であれば、postData.commentsにcommentTextをappend(追加)する
                postData.comments.append("\(commentText)\n")
            }
    
            // 増えたcommentsをFirebaseに保存する
            let postRef = Database.database().reference().child(Const.PostPath).child(postData.id!)
            //comments辞書
            let commentDictionary = ["comments": postData.comments]
            //Firebaseに辞書を保存する
            postRef.updateChildValues(commentDictionary)
        }
    
        //キーボードを閉じる
        @IBAction func textField(_ sender: Any) {
            commentTextField.text = (sender as AnyObject).text
        }
    
        // セル内のボタンがタップされた時に呼ばれるメソッド
        @objc func handleGoodButton(_ sender: UIButton, forEvent event: UIEvent) {
            print("DEBUG_PRINT: goodボタンがタップされました。")
    
            // タップされたセルのインデックスを求める
            let touch = event.allTouches?.first
            let point = touch!.location(in: self.commentTableView)
            let indexPath = commentTableView.indexPathForRow(at: point)
    
            // 配列からタップされたインデックスのデータを取り出す
            let postData = postArray[indexPath!.row]
    
            // Firebaseに保存するデータの準備
            if let uid = Auth.auth().currentUser?.uid {
                if postData.isLiked2 {
                    // すでにいいねをしていた場合はいいねを解除するためIDを取り除く
                    var index = -1
                    for likeId in postData.likes2 {
                        if likeId == uid {
                            // 削除するためにインデックスを保持しておく
                            index = postData.likes2.firstIndex(of: likeId)!
                            break
                        }
                    }
                    postData.likes2.remove(at: index)
                } else {
                    postData.likes2.append(uid)
                }
    
                // 増えたgoodをFirebaseに保存する
                let postRef = Database.database().reference().child(Const.PostPath).child(postData.id!)
                let goods = ["likes2": postData.likes2]
                postRef.updateChildValues(goods)
    
            }
        }
    }
    
[/code]


CC BY-SA 4.0
2020-01-13T14:36:18.830
62215
2020-01-13T23:28:19.907
2020-01-13T23:28:19.907
3060
37426
question
swift swift5
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x104190304)のエラーが発生します


この回答投稿時点で[別の問題](https://ja.stackoverflow.com/q/62217/13972)も発生しているコードのようですが、焦らずに一つずつ解決していきましょう。

* * *

まず、最初に理解しておくことは、iOSがブレークポイントも置いていないところで`EXC_BREAKPOINT`のCPU例外の発生を伝えてきたら、十中八九、
**Swiftが意図的にアプリをクラッシュさせるために仕込んだコードを実行している** と言うことです。

と言う訳で、そのつもりで発生箇所を見てみます。

[code]

            let editingTextFieldY: CGFloat = (self.activeTextField?.frame.origin.y)!
    
[/code]

強制アンラップの`!`が存在する行ですから、もうほぼ確実に「値が`nil`なのに強制アンラップをしているせいだ」と疑ってみるべきでしょう。

そもそもこのコード、最終的に強制アンラップしているくせに、カッコ内ではオプショナルチェイニングを使っていると言う意味のない書き方になっています。(ネットで見つけてこられたのでしょうが、残念ながらこのコードのように「意味のない書き方」を含むようなコードが検索結果の上位に来ることはよくあります…。)最終的に強制アンラップするなら、最初から`let
editingTextFieldY = self.activeTextField!.frame.origin.y`と書いた方がずっとわかりやすいですね。

(ちなみに幾つかのメソッドに明示的に`internal`を付けているのもほとんど意味が無い。)

ただし、あなたに「アプリをクラッシュさせたい」と言う意図があるのでない限り、強制アンラップは可能な限り使わない方が良いでしょう。

Swiftでは`nil`を(と言うか`Optional`型を)安全に扱う手段がいくつか用意されていますが、ここでは、「`activeTextField`が`nil`なのにこの行が実行されてしまっている」と言う状態なので`guard`で条件付きバインディングを使うのが妥当でしょう。

[code]

            guard let editingTextFieldY = self.activeTextField?.frame.origin.y else {
                print("activeTextField is nil")
                return
            }
    
[/code]

`self.activeTextField`が`nil`なんてことになっている場合、メッセージを出力して処理を中断します。

* * *

で、なぜ`self.activeTextField`が`nil`になってしまっているのかと言うのを考えると、

`UIResponder.keyboardWillShowNotification`の通知が来る前に`textFieldShouldBeginEditing(_:)`が呼ばれているはず  
↓  
`textFieldShouldBeginEditing(_:)`の中で`self.activeTextField`がセットされている

ってことから、「`UIResponder.keyboardWillShowNotification`の通知を処理するときには、`self.activeTextField`には値がセットされているはずだ」と言う結論になってしまいます…。

iOSのイベントや通知の順番は結構気まぐれで、バージョンアップの際に突然変更されることがあって、私もそれほど詳しくは無いのですが、「通知や`delegate`の設定を`viewWillAppear(_:)`の中でやっているのが遅すぎる」と言うのが一つの可能性として考えられます。

「`postData`が届いてからでないと意味が無い」と言う処理以外は、`viewDidLoad(_:)`でやってしまった方が良いでしょう。

[code]

        override func viewDidLoad() {
            super.viewDidLoad()
    
            commentTableView.delegate = self
            commentTableView.dataSource = self
            // テーブルセルのタップを無効にする
            commentTableView.allowsSelection = false
            let nib = UINib(nibName: "CommentTableViewCell", bundle: nil)
            commentTableView.register(nib, forCellReuseIdentifier: "Cell2")
    
            // テーブル行の高さをAutoLayoutで自動調整する
            commentTableView.rowHeight = UITableView.automaticDimension
            // テーブル行の高さの概算値を設定しておく
            // 高さ概算値 = 「縦横比1:1のUIImageViewの高さ(=画面幅)」+「いいねボタン、キャプションラベル、その他余白の高さの合計概算(=100pt)」
            commentTableView.estimatedRowHeight = UIScreen.main.bounds.width
    
            //textFieldがキーボードで隠れないようにする
            self.commentTextField.delegate = self
            self.setUpNotificationForTextField()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            guard let postData = postDataReceived else {
                return
            }
    
            //イメージ画像
            self.articleImage.image = postData.image
            //キャプションのテキスト
            self.articleLabel.text = postData.caption ?? "" //強制アンラップは出来るだけ使わない
            //いいねの数
            let likeNumber = postData.likes.count
            articleLilkeCount.text = "\(likeNumber)"
    
    
            if Auth.auth().currentUser != nil {
                //...
            } else {
                //...
            }
        }
    
[/code]

* * *

他の原因で`EXC_BREAKPOINT`が出ることがない訳では無いんで、100%確実とは言えないんですが、是非お試しください。


CC BY-SA 4.0
2020-01-13T17:31:30.020
62219
2020-01-13T17:31:30.020
13972
62215
answer