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

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

ただいまの
回答率

88.59%

Realmのスレッド間でのデータの受け渡しを可能にするためのスレッドセーフ参照が使用できない

解決済

回答 2

投稿 編集

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

swifty

score 36

実現したいこと

アプリ内課金で購入されたSubscription商品のレシート検証の際、realmを使おうとするとクラッシュしてしまうのでしないようにしたいです。

エラーメッセージ

Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.

該当のソースコード

func validateReceipt(completion: @escaping (Bool) -> Void) {

    //(中略)

    // 自前のサーバーを経由してAppleに問い合わせて取得したレシート情報が以下の receipt 
    verifier.verify { receipt in
        guard let receipt = receipt else {
            return
        }       

        // レシート検証ロジック
        //1. 正当なレシートか検証
        if receipt["status"] as! Int == 0 {

            //2. 自分のアプリのレシートか検証
            let receiptDictionary = receipt["receipt"] as! NSDictionary
            if receiptDictionary["bundle_id"] as! String == "com.〇〇.〇〇" {

                //3. 未登録のレシートか最新のexpires_date_msがRealmにあるか否かで検証

                let latestReceiptInfoArray = receipt["latest_receipt_info"] as! Array<Dictionary<String, AnyObject>>

                var latestExpireDate:Int = 0
                for latestReceiptInfo in latestReceiptInfoArray {
                    let receiptExpireDateMs = Int(latestReceiptInfo["expires_date_ms"] as? String ?? "") ?? 0
                    let receiptExpireDateS = receiptExpireDateMs / 1000
                    if receiptExpireDateS > latestExpireDate {
                        latestExpireDate = receiptExpireDateS
                    }
                }

                //RealmでlatestExpireDateで検索
                let predicate = NSPredicate(format: "expireDate == %i",latestExpireDate)
                let fetchedSubscriptionDataArray = self.realm.objects(Subscription.self).filter(predicate)

                if fetchedSubscriptionDataArray .isEmpty {

                    //4. 有効期限内か確認

                    let now = Int(Date().timeIntervalSince1970)

                    //有効期限以内であればレシート検証結果OK
                    if latestExpireDate > now {

                        //Subscriptionを始めたことをRealmに保存
                        let subscription = Subscription()
//                            subscription.setValue(true, forKey: "valid")
//                            subscription.setValue(Date(), forKey: "startedTime")
//                            subscription.setValue(Date(), forKey: "updatedTime")
//                            subscription.setValue(latestExpireDate, forKey: "expireDate")

                        let subscriptionRef = ThreadSafeReference(to: subscription)
                        DispatchQueue(label: "background").async {
                            let realm = try! Realm()
                            guard let subscription = realm.resolve(subscriptionRef) else {
                                return
                            }
                            try! realm.write {
                                subscription.setValue(true, forKey: "valid")
                                subscription.setValue(Date(), forKey: "startedTime")
                                subscription.setValue(Date(), forKey: "updatedTime")
                                subscription.setValue(latestExpireDate, forKey: "expireDate")
                            }
                        }

//                            do {
//                                try self.realm.write {
//                                    self.realm.add(subscription)
//                                }
//                            } catch {
//                                print("Error saving \(error)")
//                            }

                        completion(true)

                    } else {
                        print("Invalid Receipt (This receipt expired)")
                        completion(false)
                    }
                } else {
                    print("Invalid Receipt (This receipt was already used)")
                    completion(false)
                }
            } else {
                print("Invalid Receipt (Other product receipt)")
                completion(false)
            }
        } else {
            print("Invalid Receipt")
            completion(false)
        }
    }
}


以下トランザクション内の上記ファンクションが使われる該当箇所

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {

    for transaction in transactions {

        if transaction.transactionState == .purchased {

            print("Transaction successful!")

            //レシートを検証
            validateReceipt { (Bool) in
                if Bool == true {

                    // MainPageに遷移
                    self.performSegue(withIdentifier: "welcomeToMain", sender: nil)
                    SKPaymentQueue.default().finishTransaction(transaction)

                } else {

                    print("Receipt Validation Failed")

                }
            }   
        }
        //(中略)
    }
}

試したこと

始めは上のソースコードでコメントアウトしているいつも自分が使っている方法でrealmにデータを保存しようとしていたのですが上記のエラーメッセージが出てしまったため、realmのこちらのドキュメントを参照して、スレッド間でのデータの受け渡しを可能にするスレッドセーフ参照を使えばいいのではないかと考え、上のソースコードのように書きかえました。

ですが同じエラーメッセージが出て来てしまい先に進めません。

皆様のお力添えいただけますと幸いです。
何卒よろしくお願いいたします。

補足情報

Swift 5.0.1
Xcode 10.3
Realm 3.17.3

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 2

checkベストアンサー

+1

コメントアウトしている箇所のself.realmインスタンスを生成したスレッドと、verifyer.verifyのクロージャーが実行されるスレッドが異なるため、self.realmインスタンスを用いた時にエラーが出るのではないでしょうか?

self.realmではなく新しくrealmインスタンスを生成してやってみるとどうでしょうか?

verifier.verify {
    ...

    //Subscriptionを始めたことをRealmに保存
    let subscription = Subscription()
    subscription.setValue(true, forKey: "valid")
    subscription.setValue(Date(), forKey: "startedTime")
    subscription.setValue(Date(), forKey: "updatedTime")
    subscription.setValue(latestExpireDate, forKey: "expireDate")

    do {
        try self.realm.write { // `self.realm`インスタンスを生成したスレッドと`verifier.verify`に渡すクロージャーが実行されるスレッドが異なるためエラーが出ている?
            self.realm.add(subscription)
        }
    } catch {
        print("Error saving \(error)")
    }

    ...
}

verifier.verify {
    ...

    //Subscriptionを始めたことをRealmに保存
    let subscription = Subscription()
    subscription.setValue(true, forKey: "valid")
    subscription.setValue(Date(), forKey: "startedTime")
    subscription.setValue(Date(), forKey: "updatedTime")
    subscription.setValue(latestExpireDate, forKey: "expireDate")

    do {
        let realm = try Realm()
        try realm.write {
            realm.add(subscription)
        }
    } catch {
        print("Error saving \(error)")
    }

    ...
}

ThreadSafeReferenceはRealmに管理されているオブジェクトを別のスレッドに引き渡す時に用いるものなので、Subscriptionインスタンスを生成してすぐはRealm管理下にはない為にエラーが起きるのではないでしょうか

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/08/06 18:06

    ご回答ありがとうございました!

    おっしゃる通りでした。新しくインスタンスを生成することで無事解決できました。

    他の作業に追われていて検証に時間がかかり返事をするのが遅くなってしまいました。
    申し訳ございません。

    今後とも何卒よろしくお願いします。

    キャンセル

0

読み間違えていました
以下は間違いです。


ご提示のコードのみではデータの流れが全然見えないので憶測ですが ThreadSafeReference(_:) すべきなのはpaymentQueue(_:updatedTransactions:)の中の方ではないですか?

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/07/27 15:15

    お世話になっております。ご回答ありがとうございます。

    もしそうだと仮定した場合ThreadSafeReference(_:)をどちらでどのように書きかえればよいのでしょうか?

    paymentQueue(_:updatedTransactions:)の中でrealmを使っているのがvalidateReceiptの中で

    ①expireDateでrealm内のデータを検索する部分
    ②realmにデータを保存する部分

    の2つだけだと自分では認識しているのでこちらで使っているSubscriptionオブジェクトをThreadSafeReference(_:)すべきなのではないだろうかと考えこのように実装しました。

    もしもう少しこちらの詳細がわかれば回答できるなどありましたらお伝えいただければできるだけ追記などさせていただきます。

    お忙しいところ大変恐縮ですが引き続きご教授いただけますと幸いです。
    何卒よろしくお願いします。

    キャンセル

  • 2019/07/27 15:31

    すみません。 コードを読み間違えていました。

    キャンセル

  • 2019/07/27 15:55

    いえいえ。

    ご回答いただけただけで大変嬉しく思います。

    もし何かお分かりになることございましたらよろしくお願いします。

    キャンセル

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

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

関連した質問

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

  • トップ
  • Xcodeに関する質問
  • Realmのスレッド間でのデータの受け渡しを可能にするためのスレッドセーフ参照が使用できない