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

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

ただいまの
回答率

88.33%

【swift】受け取ったJSONデータをパースできない(もしくは格納できていない)

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,802

Tajiko

score 12

前提・実現したいこと

iOSアプリを作成しています。
ライブハウスの名前を検索して地図上に周辺のコインロッカーを表示させるアプリです。
場所の情報(検索ワード、経度、緯度、詳細)のデータベースを仮想サーバに作り、
サーバー側にPHPファイルを起いて自作APIを作り、JSON形式で出力するようにしています。

発生している問題・エラーメッセージ

swift側でJSONのパースが上手くできないのか、それとも構造体への格納ができないのか
do try catch エラーハンドリングで必ずエラーをキャッチしてしまいます。

http://192.168.33.10:8000/api.php/?livehouse=%E6%9C%AC%E5%85%AB%E5%B9%A1
2019-02-21 17:41:49.762064+0900 WhereistheCoinLocker[10697:390643] TIC Read Status [1:0x600000ae1380]: 1:57
php output: Optional("[{\"latitude\":35.720825,\"longitude\":139.927428,\"keyword\":\"JR本八幡駅改札付近北口方面\"},{\"latitude\":35.7211,\"longitude\":139.927267,\"keyword\":\"都営新宿線本八幡駅 地下1階 駅長事務室の左右\"},略 検索結果が続きます]")
エラーがでました

該当のソースコード

//jsonの中身を受け取るデータ構造
    struct Coinlocker: Codable {
        let latitude : Double?
        let longitude : Double?
        let keyword : String?
    }

    //jsonのデータ構造
    struct ResultJson: Codable {
        //複数要素
        let item:[Coinlocker]?
    }

    //ロッカーを検索するためPHPにリクエストするメソッド
    func searchLocker (keyword: String) {

        //検索キーワードをURLエンコードする
        guard let keyword_encode = keyword.addingPercentEncoding(withAllowedCharacters: sNSCharacterSet.urlQueryAllowed) else {
            return
        }

        //リクエストURLの組み立て
        guard let req_url = URL(string: "http://192.168.33.10:8000/api.php/?livehouse=\(keyword_encode)") else {
            return
        }
        print(req_url)

        //リクエストに必要な情報を生成
        let req = URLRequest(url: req_url)

        //データ転送を管理するためのセッションを生成
        let session = URLSession(configuration: .default, delegate: nil
            , delegateQueue: OperationQueue.main)
        //リクエストをタスクとして登録
        let task = session.dataTask(with: req, completionHandler: {
            (data , response , error) in

            //セッションを終了
            session.finishTasksAndInvalidate()
            //do try catch エラーハンドリング
            do {
                //data にちゃんと検索結果が入っているらしい
                let phpOutput = String(data: data!, encoding:  .utf8)
                print("php output: \(String(describing: phpOutput))")

                //JsonDecoderのインスタンス取得
                let decoder = JSONDecoder()
                //受け取ったJSONデータをパース(解析)して格納
                let json = try decoder.decode(ResultJson.self, from: data!)

                print(json)

            } catch {
                //エラー処理
                print("エラーがでました")
            }
        })
        //ダウンロード開始
        task.resume()
    }
<?php

if(isset($_GET['livehouse'])) {

    $livehouse = htmlspecialchars($_GET['livehouse']);

define("USERNAME","xxxxx");
define("PASSWORD", "xxxxxx");

try{
    $dbh = new PDO("mysql:host=192.168.33.10;dbname=coinlockerdb;
    charset=utf8", USERNAME, PASSWORD);

    $dbh-> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $dbh-> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        $stmt = $dbh->prepare("select latitude, longitude, keyword from coinlockers where livehouse like '%$livehouse%'");
    $stmt-> setFetchMode(PDO::FETCH_ASSOC);
    $stmt-> execute();
    $rows = array();
    while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
        $rows[] = $row;
    }
    # 接続成功ならjson形式で吐き出す
    header('Content-Type: application/json; charset=UTF-8');
    echo $json = json_encode($rows, JSON_UNESCAPED_UNICODE);
    return $json;
} catch (PDOException $e){
    echo "JSON吐き出し失敗";
    echo $e->getMessage();
}
$dbh =null;
}else{
    echo "失敗";
}

試したこと

swift側の構造体の変数名とデータベースの各項目のスペルが間違っていないか確認しました。
デバッグエリアに出ている TIC Read Status [1:0x600000ae1380]: 1:57についても検索してみましたがこれはエラーではないのでしょうか。調べてもよくわかりませんでした。
初心者でどこがおかしいのか、何を調べていいのかもわからない状態です。

補足情報(FW/ツールのバージョンなど)

データベースのテーブルの情報は下記の通りです。
+-----------+------------------+------+-----+---------+----------------+
| Field     | Type             | Null | Key | Default | Extra          |
+-----------+------------------+------+-----+---------+----------------+
| id        | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| latitude  | double           | NO   | UNI | NULL    |                |
| longitude | double           | NO   | UNI | NULL    |                |
| livehouse | varchar(50)      | NO   |     | NULL    |                |
| keyword   | text             | NO   |     | NULL    |                |
+-----------+------------------+------+-----+---------+----------------+

参考にしたものはこちらです。(たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 10 Swift 4.2対応 より)

 //Jsonのitem内のデータ構造
    struct ItemJson: Codable {
        //お菓子の名前
        let name: String?
        //メーカー
        let maker: String?
        //掲載URL
        let url: URL?
        //画像のURL
        let image: URL?
    }

    //JSONのデータ構造
    struct ResultJson: Codable {
        //複数要素
        let item:[ItemJson]?
    }

    //searchokashiメソッド
    func searchOkashi (keyword: String) {

        //お菓子の検索キーワードをURLエンコードする
        guard let keyword_encode = keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
            return
        }

        //リクエストURLの組み立て
        guard let req_url = URL(string:
            "http://www.sysbird.jp/toriko/api/?apikey=guest&format=json&keyword=\(keyword_encode)&max=10&prder=r") else {
                return
        }
        print(req_url)

        //リクエストに必要な情報を生成
        let req = URLRequest(url: req_url)
        //データ転送を管理するためのセッションを生成
        let session = URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue.main)
        //リクエストをタスクとして登録
        let task = session.dataTask(with: req, completionHandler: {
            (data, response, error) in
            //セッション終了
            session.finishTasksAndInvalidate()
            //do try catcyエラーハンドリング
            do{
                //JSONDecoderのインスタンス取得
                let decoder = JSONDecoder()
                //受け取ったJSONデータをパース(解析)して格納
                let json = try decoder.decode(ResultJson.self, from:data!)


                //お菓子の情報が取得できているか確認
                if let items = json.item {
                    //お菓子のリストを初期化
                    self.okashiList.removeAll()
                    //取得したお菓子の数だけ処理
                    for item in items {
                        //お菓子の名前、メーカ名、掲載URL,画像URLをアンラップ
                        if let name = item.name, let maker = item.maker, let link = item.url, let image = item.image {
                            //1つのお菓子をタプルでまとめて管理
                            let okashi = (name, maker, link, image)
                            //お菓子の配列へ追加
                            self.okashiList.append(okashi)
                        }
                    }
                    //TableView を更新する
                    self.tableView.reloadData()
                    if let okashidbg = self.okashiList.first {
                        print ("-----------------")
                        print ("okashiList[0] = \(okashidbg)")
                    }
                }

            } catch {
                //エラー処理
                print("エラーが出ました")
            }
        })
        //ダウンロード開始
        task.resume()
    }

    //Cellに値を設定するdatasourceメソッド。必ず記述。
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        //お菓子リストの総数
        return okashiList.count
    }

    //Cellに値を設定するdatasourceメソッド。必ず記述。
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        //今回表示を行う、Cellオブジェクト(1行)を取得
        let cell = tableView.dequeueReusableCell(withIdentifier: "okashiCell", for: indexPath)
        //お菓子のタイトル設定
        cell.textLabel?.text = okashiList[indexPath.row].name
        //お菓子の画像を取得
        if let imageData = try? Data(contentsOf: okashiList[indexPath.row].image) {
            //正常に取得できた場合は、UIImageで画像オブジェクトを生成してCellにお菓子画像を設定
            cell.imageView?.image = UIImage(data: imageData)
        }
        //設定済みのCellオブジェクトを画像に反映
        return cell
    }

    //Cellが洗濯された際に呼び出されるdelegateメソッド
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // ハイライト解除
        tableView.deselectRow(at: indexPath, animated: true)

        //SFSafariViewを開く
        let safariViewController = SFSafariViewController(url: okashiList[indexPath.row].link)

        //dekegate の通知先を自分自身
        safariViewController.delegate = self

        //SafariViewが開かれる
        present(safariViewController, animated: true, completion: nil)
    }

    // SafariViewが閉じられた時によばれるdelegateメソッド
    func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
        // SafariViewを閉じる
        dismiss(animated: true, completion: nil)
    }
  • 気になる質問をクリップする

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

質問への追記・修正、ベストアンサー選択の依頼

  • Tajiko

    2019/02/21 18:45

    ありがとうございます。
    早速catch let error { print(error); }としてみましたら、下記エラーが出ました。
    typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
    構造体の中のl keyword が String型 ではおかしいということでしょうか。

    また、参考にしたものは「たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 10 Swift 4.2対応」という書籍です。質問に追加いたします。

    キャンセル

  • Tajiko

    2019/02/21 19:12

    FKMさま
    ありがとうございます。
    ご指摘の通り確認しましたところ、JSONの形式は正しいようでした。

    キャンセル

  • FKM

    2019/02/21 19:27 編集

    あと、PHPで少し変な文章があったので修正しました。そして自分も勘違いがあったんですが
    catch(PDO::Exception $e) は名の通り、PDOの接続エラーしか返してくれません。ですので、
    json_encode()の不備を調べるためには二重にtry-catchが必要ですね
    try{
    $data = json_encode($rows, JSON_UNESCAPED_UNICODE);
    }catch( Exception $err ){
    $data = $err -> getMessage();
    }
    echo $data;
    //return $json; いらない

    キャンセル

回答 1

checkベストアンサー

+1

そのコードだと

JSONは、

{
    "item": [{
        "latitude": 35.720825,
        "longitude": 139.927428,
        "keyword": "JR本八幡駅改札付近北口方面"
    }, {
        "latitude": 35.7211,
        "longitude": 139.927267,
        "keyword": "都営新宿線本八幡駅 地下1階 駅長事務室の左右"
    }]
}

になります。

そのJSONだと

コードは、

//let json = try decoder.decode(ResultJson.self, from: data!)
let json = try decoder.decode([Coinlocker].self, from: data!)

になります。

オマケ

エラー表示しましょう。キッカケくらいは掴めるかも知れません。

//print("エラーがでました")
print(error)
余談

ライブハウスだからCoin Rockerとか...

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/02/21 19:21

    fuzzballさま
    ありがとうございます。
    ご指摘の通りコードを修正しました所、無事にパース、格納でき、デバッグエリアに格納されたデータを表示することが出来ました。ありがとうございました。

    キャンセル

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

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

関連した質問

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