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

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

ただいまの
回答率

88.92%

【Swift】カスタムクラスの配列要素が正しく更新されない

解決済

回答 1

投稿

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

ishicoro

score 24

本件で3週間詰まってしまっており、お力添え頂けますと大変幸いです。

【実現したいこと】
メッセージアプリを作っています。
基本的な処理の流れは下記の通りです。
1.各友達との個別のトーク画面を開く
2.Firebaseからその友達とのメッセージ情報を取得し、カスタムクラスの配列に代入
3-a.Collection Viewのセルをタップすると、カスタムクラスから該当の情報を取得し再生
3-b.iPhoneのProximity Sensorが発動した際も、3-aと同様に、画面に表示されているセルの音声を自動再生(1画面に表示されるセルは1つのみ。)

【問題点】
3-a.は問題なく処理できますが、3-b.でつっかかっています。アプリを起動し、最初に開いたトーク画面で処理をする分には上手くいきますが、一度個別トークルームを出てから、もう一度そのトークルーム、あるいは別の友達のトークルームに行ってからProximity Sensorを発動すると、正しいメッセージと、先ほどいたトークルームのメッセージの2つが同時に再生されてしまいます。message配列は個別トークルームを開くたびに作り直されると思っていましたが、なぜか前回入ったトークルームの情報を踏襲してしまっています。踏襲した上で、新しい方のトークルームの情報も追加されるという謎現象です。

  @objc func proximitySensorStateChanged(){
        print(messages[1].url)

        if (UIDevice.current.proximityState == true) {
            do{
                let audioSession = AVAudioSession.sharedInstance()
                do {
                    try audioSession.overrideOutputAudioPort(AVAudioSession.PortOverride.none)
                } catch let error as NSError {
                    print("audioSession error: \(error.localizedDescription)")
                }
                    let audioUrl = URL(string: messages[self.neo].url!)//self.neoには画面に現在表示されているセルのindexPath.rowの数字が入っています。(別箇所で取得済み)。
                    URLSession.shared.dataTask(with: audioUrl!, completionHandler: { (data, response, error) in
                        DispatchQueue.global().async{
                            DispatchQueue.main.async{
                                do {
                                    self.audioPlayer =  try AVAudioPlayer(data:data!)
                                    self.audioPlayer.delegate = self
                                    self.audioPlayer.play()
                                }
                                catch{}
                            }
                        }
                    }).resume()
             } catch {
            }
        }
    }

【試してみたこと】
一度入ったトークルームから戻るときに、removeAllでmessage配列を空にしてみました。

 @IBAction func backBtn(_ sender: Any) {
        messages.removeAll()
        dismiss(animated: true, completion: nil);
    }

この状態で、もう一度誰かのトークルームに入ると、 promximity sensorの上記コードで記載した一番最初のprint(messages[1].url)がエラーとなりクラッシュしてしまいます。一度messageを空にしても、今回新しいトークルームに入るタイミングでその情報を取得しmessageに入れているはずなのでnilにならないはずなのですが。。。
またこの状態で、以下didselectの中で同じ出力をしてもクラッシュしません。didselectで処理する分には、いろんなトークルームに出入りしようがバグは起きず、ちゃんと該当のメッセージが再生されます。

 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print(messages[1].url)
}

なお、Firebaseから情報を取得してカスタムクラスに代入しているコードは以下です。

 var messages = [Message]()

//override func viewDidLoad()の中
 Database.database().reference().child("user-messages").child(uid!).observe(.childAdded, with: { (snapshot) in
            let messageId = snapshot.key
           DBProvider.Instance.mediaMessagesRef.child(messageId).observe(.value, with: { (snapshot) in
                if let d = snapshot.value as? [String:AnyObject]{
                        if let url = d["url"] as? String {
                            let senderId = d["sender_id"] as? String
                            let toId = d["toID"] as? String
                            let childkey = d["childkey"] as? String
                            let listened = d["listened"] as? Bool
                            let timeStamp = d["timeStamp"] as? NSNumber
                            let newContact = Message(senderId: senderId!, url: url, toId: toId!, timeStamp: timeStamp! , childkey: childkey!, listened: listened!);
                            //自分が送ったボイスかつ、送り先がContactsVCで選んだ友達のとき。
                            if senderId == (Auth.auth().currentUser?.uid)! && toId! == self.toID {
                                self.messages.append(newContact);
                            }
                                //友達が送ったボイスかつ、送り先が自分
                            else if senderId == self.toID && toId == Auth.auth().currentUser?.uid {
                                self.messages.append(newContact);
                            } else {
                              }
                            //自分からそれ以外に送ったボイスや、友達が自分以外に送ったボイスはスルーする。
                           self.messages.sort(by: {Double($0.timeStamp!) < Double($1.timeStamp!)})
                        }
                }
            }, withCancel: nil)
        }, withCancel: nil)


NotificationCenter.default.addObserver(self, selector: #selector(ChatVC.proximitySensorStateChanged), name: UIDevice.proximityStateDidChangeNotification, object: nil)

以上、お読みいただきありがとうございます。
アドバイス等ございましたらコメント頂けますと幸いです。よろしくお願いいたします。

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 1

checkベストアンサー

0

ちょっと全体像がわかりませんが、
「message配列は個別トークルームを開くたびに作り直される」と書いてありますが、
viewDidLoadに記載しているとのことでしたよね?

print()を追記し、確認していただければわかると思いますが、
きっと、viewDidLoadはviewを生成する時(メモリに保持する時)のみで、
画面を表示するたびに実行されていないと思いますが、、、

通常表示を毎回読み込みたいコードを書くなら、
viewWillAppearやviewDidAppearではないですか???

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/09/23 17:01

    補足させてください。

    Aくん、Bくんのの二人とのトークがあり、Aくんとのメッセージ数は18個、Bくんとのメッセージ数は10個です。

    Aくん、BくんのそれぞれのトークルームのView ControllerのViewdidappearの中で、messages.countを出力すると、それぞれ18と10が出力され、collectionview上でも同数のセルが表示されています。

    ここで、Aくんのトークルームに入った状態でiPhone上部のセンサーを手で隠し、proximitysensorを発動させます。selectorの中でprint(messages.count)と出力をすると、18個と出力されます。ここまでは問題ありません。一度Aくんのトークを出て、Bくんのトークに入りなおします。この状態でproximitysensorを発動させると、print(messages.count)は、18と10が連続で出力されます。(ここはてっきり1回で28が出力される思っていました。)

    順番としてまず18を出力してから10を出力します。確かに音声を聞いても、先にAくんのメッセージが流れ初めて、途中ですぐかぶせる形でBくんの音声が流れ始めます。message配列の箱が二つ作成されるのかと思います。

    最初にAくんの部屋に入ってから戻るときにmessages.removeAll()を加えると、次にB君の部屋に入ってproximitysensorを発動させたときにmessages.countが0となりself.neoのところでクラッシュします。Aくんとのメッセージ内容を全て取り除いても、Bくんの部屋に入ったらBくんとのメッセージ情報を取得するはずなので、ここで配列の中身が空になることが今まで不思議でした。

    ですが上記から察するに、一度Aくんのmessagesを参照しに行ってから、次にBくんのmessagesを参照しに行っているので、Aくんのmessagesはremoveで0になると、たとえBくんのmessagesに情報が入っていたとしても、Aくんの箇所で先にクラッシュしてしまうのかと思います。

    didselectやviewdidapperなどでmessages.countを出力する分には異常がないので、selectorを使用してselectorの中でmessagesにアクセスしに行こうとするとおかしいことになるのかと推察します。

    なお、selectorを呼び出す下記を記載する位置を色々変えてみても結果は変わらずでした。

    NotificationCenter.default.addObserver(self, selector: #selector(ChatVC.proximitySensorStateChanged), name: UIDevice.proximityStateDidChangeNotification, object: nil)

    もし何かお気付きでしたらご指摘頂けますと幸いです。
    なければそのままベストアンサーとさせてください。
    私の方でももう少し色々試してみます。

    キャンセル

  • 2019/09/28 22:10 編集

    お話を聞くとさらに全体像をみてみたくなりました。

    しかし、messageの作り方に問題があるのは確かです。
    通常、Firebaseからデータを取得する場合、
    当然ネットからの取得なので、非同期処理となります。

    なので、backBtn でmessagesをremoveAll()とすると、
    messagesは空配列になります。
    そして、入り直した時、viewDidLoadでダウンロードの処理が開始されますが、
    非同期なので、完了を待たずに、viewを作り出す事になります。
    なので、collectionViewがmessagesなしの状態で作り始められてしまいます。
    その結果、url は nilとなり、クラッシュします。

    通常だと、待ってからcollectionViewを読み込むのは無理があるので、
    まず、""や0などのデータがない状態にセットしておき、
    firebaseからデータの読み込みが完了した時点で、
    再度、collectionViewをreloadします。
    numberOfItemsInSectionやcellForItemAtを走るように適当にうめたあとで、
    各func の最初の行にprint("[***(function名)***]messages:", messages)
    firebaseのデータ読み込みの{}の中にprint("firebase完了")を書き込んでみたら、
    読み込み完了のタイミングがわかると思います。

    解決策としては、
    AくんやBくんの各々のメッセージ表示画面の一つ上の階層(view)は
    ラインの「トーク」画面などのように
    全部の会話が日付順にリスト表示されていると思います。
    例)
    2019/9/26 Aくん
    2019/9/24 Bくん

    そのviewとかで自分が出した、自分宛にきたデータの全部をDLし、
    個別メッセージを開くときはその中から該当の相手のデータのみをfilterし、
    pushする方法にした方がいいのではないですか?

    キャンセル

  • 2019/09/28 23:02

    hamejiさん
    ご丁寧にご指南いただきありがとうございます。
    実は先ほど、下記方法でなんとか解決いたしました...!
    戻るときにVCを解放できていなかったことが問題でした。
    (ただ、didselectやviewdidappear内のprintでも同じはずなのに、なぜ問題がなかったかは謎ですが)

    https://teratail.com/questions/214257

    キャンセル

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

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

関連した質問

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