読者です 読者をやめる 読者になる 読者になる

Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 497 - JSON住所録 : チュートリアル

2017年4月8日:Swift 3に対応*1

JSONとは?

JavaScript Object Notationの頭文字です。なんとJavaScriptで取り決められているデータ構造です。Webサービスで情報のやりとりをするときに可読性が優れていることで標準になっています。

表記法も次のようなものがあります。

[ data, data, data, ..... ]

これは明らかにSwiftのArray型と同様ですね。
そして、JSONでもSwiftと同様なDictionary(辞書)型も取り扱えます。しかし、「[]」を使わないで「{}」を使っているので混乱しそうです(^^;)

{ "Key": "Value", "Key": "Value", "Key": "Value", .... }

"Key"は文字列というルールもあります。どんな型でもとれるSwiftのDictionary型と異なる点です。

他にも「辞書の配列」や「辞書の辞書」を使うのが一般的なようです。

[ { "aaa": "34", "bbb": "35", "ccc": "36" }, 
  { "123": "true", "234": "false", "345": "true" } ]

[]と{}の使い方の違いに慣れる必要があります。JSONの扱いはネットワークでデータを扱う時に必須です。少なくともこの教科書で説明しているデータの書き方は熟知しましょう。

{ "key1": { "aaa": "34", "bbb": "35", "ccc": "36" },
  "Key2": { "123": "true", "234": "false", "345": "true" } } 

辞書のValueに辞書データを持つこともできるため、頭が混乱しそうになります(^^;) しかし、次のようなデータ構造が一般的かも。

[
  {
    "person": {
      "name": "Yuji",
      "age": "51"
    }
  },
  {
    "person": {
      "name": "Miyuki",
      "age": "36"
    }
  }
]

SwfitでJSONの扱い

JSONデータを扱うクラスが用意されています。それが「JSONSerialization」です。

初めて聞く用語もありました!「パース(Parse)」です。新しい知識が増えるって嬉しいですね。Swift以外のデータ構造を解析してSwiftのデータ構造に取りこむという意味です。

JSONのデータ構造は「配列」と「辞書」なので、NSJSONSerializationクラスを使ってSwiftの言語体系に取りこんでから配列と辞書にキャストさせます。このクラス名にあるSerialization(シリアライゼーション)は、ここで説明しているようにバイナリ化するという意味があります。

var anArray: [Any]?
do {
  anArray = try JSONSerialization.jsonObject(with: <JSONの配列データ>)
 // エラーが無ければ実行するコードが続きます
} catch {
  print(error)
 // errorに準じた処理(switch-caseを使うのが一般的?)
}

guard let item = anArray?.first as? [String: Any],
  let person = item["person"] as? [String: Any],
  let age = person["age"] as? Int else {
    return
}

データ構造も壊れているかもしれません。このように外部とデータをやり取りするクラスメソッドの場合、Swift 2からNSErrorタイプのエラーを投げる(throw)仕組みが実装されています。Swift 3に改訂され、多くのクラスメソッドに実装が進みました。利用時に必ずerrorを受け取る仕組み(try do-catch)も必要になります。Swift2からエラーハンドリングの機能が新しく加わっています

ここまでがJSON取り扱いの基本事項でした。

ZipToAddressチュートリアルアプリ

次にJSONデータを使ったアプリ製作の課題に取り組みます。外部にあるAPIを使用する方法なので私には初めての経験になります。

電話番号を入力すると外部のサイトに飛んでそこで住所データを取得してアプリに取りこんで表示させるという単純なアプリです。

  1. 郵便番号の入力を受け付けるUITextFieldオブジェクトと結果を表示するUILabelオブジェクトの用意
  2. 検索を実行するためのUIButtonオブジェクトを用意してWebAPIを送信する
  3. 外部から戻ってくるデータはJSONデータなので、パース(解析)してSwiftで理解できるデータに変換して表示

このアプリの核は、外部から住所検索機能を持ってくるところにあります。

http://zipaddress.net

上記は郵便番号で住所録を検索するためのAPIを提供している試験的なサイトです。いつまで存在するのか分かりませんが、この記事を書いている時点で教科書の内容と同じように存在していました。

ここの利用ですが、以下の注意点があります。

API利用規約
以下の規約に同意頂ける方のみご利用下さい。また、当サイトのサービスを利用した時点で規約に同意したものと致します。

  • 個人・法人・商用・非商用問わず無料でご利用頂けます。リンクなどの設置も必要ありません。
  • 当サイトのコンテンツの無断転用は禁止です。また、ファイルの二次配布なども禁止です。
  • APIリクエストに対するレスポンスはベストエフォートです。100%のレスポンスを保証するものではありません。
  • 当サイト内のサービスを使用したことによる一切の損害等に対し責任を負いません。
  • 当サイト内のサービスが使用できなくなったことによる一切の損害等に対し責任を負いません。
  • 作者の都合により、公開を中止する場合があります。

基本的に勉強のために作成したものです。何卒ご了承下さい。

上記のことを踏まえて利用します。

住所検索をするために利用する方法が「HTTPリクエスト」です。「http://api.zipaddress.net/」に郵便番号を付加して送信するだけです。

郵便番号「453-0809」の場合は次のようになります。

http://api.zipaddress.net/?zipcode=453-0809

最後の郵便番号ですが、ハイフン(-)があっても無くても構わないようです。

すると次のようにJSONデータが返されます。

f:id:yataiblue:20170408185657j:plain

このJSONデータをもう少し詳しく見てみます。

「code」と「data」が入っています。もう少し詳しく見るとDictionary型ですが、2つめの項目はValus値に辞書型を持った入れ子状態です。

{ Key(String): Value(Int), Key(String: { Key(String): Value(ごちゃごちゃしたデータ), Key(String): Value(ごちゃごちゃしたデータ), .... }}

上記のデータ構造がJSONデータ構造ですが、詳しい説明はパース(解析)のパートで説明します。

ZipToAddressプロジェクト

Single View Applicationを作ります。プロジェクト名は「ZipToAddress*2にします。

後の設定はデフォルト状態(今までのチュートリアルと同様です)で使用します。

まず画面デザインは次のようにします。

f:id:yataiblue:20170408195542j:plain

UITextField、UIButton、UILabelのみで構成されます。UIの設置にAutolayoutの知識は必須です。ここでその説明はしません。分からない人はSwiftで遊ぼう! - 204 - フィリングを使ってレイアウト調整(Auto Layoutのまとめ) - Swiftで遊ぼう! on Hatenaを読んでください。

シンプルなデザインの場合、それぞれのオブジェクトに位置決めのルール(コンストレイント)を与えるだけで上手くいきます。

それぞれのオブジェクトをコードで制御するためにすべきことはViewController.swiftに「Ctrl + ドラッグ」して@IBOutletプロパティと@IBActionメソッドを作る事です。

ここでは一番上に設置したUITextFiledの取り扱いだけ注意が必要です。@IBOutletと@IBActionの両方を作ります。

@IBOutlet weak var zipTextField: UITextField!

@IBAction func tapReturn() {
}

このテキストフィールドにメソッドを用意したのは、Returnキーを押したらキーボードを消すためです。「Ctrl + ドラッグ」してメソッドを作るのですが、Eventを「Did End On Exit」にしてsenderは「none」にします。このEventに色々な状況が用意されているので、それぞれを使いこなせればテキストフィールドをかなり上手に使いこなせそうです。

f:id:yataiblue:20170408195724j:plain

zipTextFieldはUITextFieldオブジェクトのインスタンス名です。アトリビュート・インスペクタにある「Placeholder」に「〒番号を入力してください」と入力して、「Keyboard Type」を「Numbers and Punctuation」に変更しておきます。

住所検索ボタンからViewControllerに「Ctrl + ドラッグ」して、「tapSearch」という@IBActionメソッドを作ります。

4つ並べてあるラベルも「Ctrl + ドラッグ」して@IBOutletを作り以下のようにプロパティとメソッドを準備します。

f:id:yataiblue:20170408200240j:plain

まずtapSearchメソッドの実装から考えます。

zapTextFieldの文字列はオプショナルなtextプロパティに格納されているため、値があるかどうか確認してアンラップします。

@IBAction func tapSearch(_ sender: UIButton) {
    guard let zipText = zipTextField.text else {
        // nilになり何の処理もされません。
        return
    }
        
    let urlString = "http://api.zipaddress.net/?zipcode=\(zipText)"
}

ここまでのステップはテキストフィールドの内容(String)を使って、リクエストするアドレスをString型で指定します。

当然ですが、このString型のアドレスを使ってUULクラスを生成します。しかし、このURLが存在するかどうか確認をしながらコードしていくので次のようにします。

if let url = URL(string: urlString) {
    let config = URLSessionConfiguration.default
    let session = URLSession(configuration: config)
    .....
    .....

実はこの部分、教科書と異なります。教科書ではURLSessionConfigurationクラスを使っていません。いきなりURLSession.sharedというシングルトン・プロパティを使っています。文字列のようなデータ量が少ない場合はパフォーマンスに違いがありませんが、画像のような大きなデートを扱う場合パフォーマンスが落ちるため、私は必ずConfigurationを使ってURLSessionを起動させます。

次はセッションのtaskを起動させます。外部からデータを習得するときに使うメソッド「dataTask()」は必ず使用法を理解していないといけません。何度もコードを自分で書いて覚えます。

let task = session.dataTask(with: url, completionHandler: onGetAddress)
task.resume()

このメソッドは引数が2つあります。最初の引数はURL型のアドレスデータは理解しやすいです。しかし、このアドレスにアクセスして得られたデータの処理を2つ目の引数でさせるのですが、ここの書き方にバリエーションがあって理解し難いんです。クロージャを使って処理のステップを一気にやってしまうというのが一般的です。しかし、この教科書のように一旦メソッドを置くやり方は初心者に理解しやすいと思います。ここで指定する「onGetAddress()」メソッドはカスタムメソッドなので、コーディングをしている時点で存在しないためエラーが出現しますが無視をします。

実は、教科書でこのメソッドに「self」が付いています。「self.onGetAddress」になっているのですが、当然のように省略可能です。明らかにViewController.swiftファイルに存在するからです*3

次にこのカスタムメソッドを書く時の注意点があります。このカスタムメソッドに3つの引数を渡さないとエラーになります。3つの引数を渡さなければならないのですが利用するかどうかはデベロッパーの自由です。

func onGetAddress(data: Data?, response: URLResponse?, error: Error?) {
    print(data)
}

取りあえずdataをコンソールに表示するメソッドにします*4

そして次に重要なステップがあります。ATS(App Transport Security)です。Swiftで遊ぼう! - 455 - Swift 2 NSURLSessionまだ全然わかっていない...ちょっと分かったかな(ATS) - Swiftで遊ぼう! on Hatenaをみて「api.zipaddress.net」を例外サイトにします。

そしてランをして、検索フィールドに「1000000」を入力して「住所検索」ボタンを押します。

f:id:yataiblue:20170408205318j:plain

Swift 2まで使用していたNSData型の場合、ここで数字が羅列したバイナリデータを確信することができたのですが、Swift 3では利用するData型ではサイズ(109 bytes)しか分かりません。これを見ただけでもセキュリティが向上しているようです。

ダウンロードした辞書データは、JSONデータになっています。

まだラベルに表示をする前にJSONデータをパース(解析)する必要があります。

「 onGetAddress()」メソッドを具体的に実装していく手順です。

func onGetAddress(data: NSData?, 
       response: NSURLResponse?, error: NSError?) {
}

このメソッドの中心になるクラスがNSJSONSerializationです。これを使ってJSONデータを「NSArray」か「NSDictionary」に変更します。

JSON住所データをもう少し詳しくみてみます。

{
"code": 200,
"data": { "pref": "\uなんだかんだ", "address": "\uなんだかんだ", "city": "\uなんだかんだ", "town": "\uなんだかんだ".... }
}

このデータはkey値が「code」と「data」の2項目が含まれるDictionary型なのでNSDictionaryにパースします。

JSONSerialization

次のJSONデータからファウンデーションオブジェクトを作る次のクラスメソッドを使います。

JSONSerialization.jsonObject(with data: Data, 
    options opt: JSONSerialization.ReadingOptions = []) throws -> Any

少し詳しくパラメータを調べてみると「options」ですが、指定できるのは次の3つだけのようです。

  • JSONReadingOptions.mutableContainers : Mutableのオブジェクトを返す
  • JSONReadingOptions.mutableLeaves : StringオブジェクトだけMutableなNSMutableStringにして返す
  • JSONReadingOptions.allowFragments : Imutableのオブジェクトを返す

戻り値のオブジェクトがMuatbleにすべきか? Imutableにすべきか? まだイマイチMutableの概念が分かっていない.... ここから勉強しないといけないのですが、すべてMutableで扱っても特に問題は無さそうなのでmutableContainersを指定します。

このメソッドは「throws」*5でerrorを投げる仕組みになっています。ということは「do catch」を使って「try」できるということです

 do {
    let jsonDic = try JSONSerialization.jsonObject(with: data!, 
                  options: .mutableContainers) as? NSDictionary
    // 具体的な処理が続きます。
   // 処理
   // 処理
    } catch let error as NSError {
        print("Error is \(error.userInfo)")
}

後はこの教科書、引数のerrorを無視していました(T_T) 具体的なエラー処理が見られると思ったのですが、「catch」でエラーを一括処理していたのでガッカリです。取りあえずコンソールでエラーを確認させます。

では具体的な処理を見ていきます。jsonDicはNSDictionary型で、2項目しかありません。「code」と「data」です。まず、key値「code」の値を抜き出します。この値は数字(Int型)なのでInt型にキャストする必要があります。ここで「guarad」を使ってInt型の値があるかどうか確認してキャストします。

guard let code = jsonDic?["code"] as? Int else {
    return
}

Int型でなければ処理は終了します。Int型であれば「200」以外はエラーコードになるのでその中で条件を見ます。

if code != 200 {
    if let errorMessage = jsonDic?["message"] as? String {
        print("エラーメッセージは" + errorMessage)
    }
}

後はcodeが200の処理なので、全てDictionary型の住所データのはずなので、NSDictionaryにキャストします。

guard let data = jsonDic?["data"] as? NSDictionary else {
    return
}

後は住所の解析です。

guard let pref = data["pref"] as? String else {
    return
}
print("県名は\(pref)です")
            
guard let city = data["city"] as? String else {
    return
}
print("市区町は\(city)です")
            
guard let town = data["town"] as? String else {
    return
}
print("町域は\(town)です")
            
guard let address = data["fullAddress"] as? String else {
    return
}
print("住所は\(address)です")

これでデータをコンソールに吐き出して確認ができました。

f:id:yataiblue:20170408223906j:plain

JSONデータの解析ができたので実際のラベルにデータを表示させるステップの前に、URSessionクラスの扱いで注意すべきことがあります。URSessionのメソッドは実行に時間がかかるのでバックグラウンド・キューで実行させて画面に表示させる時にメインスレッドに戻す仕組みを実装する必要があります。マルチスレッドが理解できていなければSwiftで遊ぼう! - 302 - マルチスレッド(まとめ) - Swiftで遊ぼう! on Hatenaを勉強しましょう。

まずdataTaskメソッド実行時にバックグラウンド・キューに入れます。ここで[unowned self]がわからなければ、メモリーマネージメントを理解する必要があります -> Swiftで遊ぼう! - 869 - ARCとメモリーマネージメント - Swiftで遊ぼう! on Hatena ARCが理解できれば、「onGetAddress」を「self.onGetAddress」に変更する理由がわかるでしょう。

DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
    let task = session.dataTask(with: url, 
                   completionHandler: self.onGetAddress)
    task.resume()
}

するとハンドラーで組み込んだメソッド「onGetAddress」でメインスレッドに戻す必要があるのでonGetAddress()を次のようにします。

func onGetAddress(data: Data?, response: URLResponse?, error: Error?) {
    print(data ?? "Error downloading!")
    do {
        let jsonDic = 
             try JSONSerialization.jsonObject(with: data!, 
                  options: .mutableContainers) as? NSDictionary
        guard let code = jsonDic?["code"] as? Int else {
            return
        }
        if code != 200 {
            if let errorMessage = jsonDic?["message"] as? String {
                DispatchQueue.main.async {
                    print("エラーメッセージは" + errorMessage)
                }
            }
        }
            
        guard let data = jsonDic?["data"] as? NSDictionary else {
            return
        }
            
        guard let pref = data["pref"] as? String else {
            return
        }
            
        DispatchQueue.main.async {
            self.prefLabel.text = pref
        }
            
        guard let city = data["city"] as? String else {
            return
        }
            
        DispatchQueue.main.async {
            self.cityLabel.text = city
        }
            
        guard let town = data["town"] as? String else {
            return
        }
            
        DispatchQueue.main.async {
            self.townLabel.text = town
        }
            
        guard let address = data["fullAddress"] as? String else {
            return
        }
            
        DispatchQueue.main.async {
            self.addressLabel.text = address
        }
        
    } catch let error as NSError {
        print("Error is \(error.userInfo)")
    }
}

これでプロジェクトのコーディングは終わりました。ランさせると次のようにデータのダウンロード成功です!

f:id:yataiblue:20170408234318j:plain

*1:2017年2月25日:Swift 3に向けて改訂中、2016年3月1日改訂:JSONの基本的事項とチュートリアルをまとめました。Swiftではじめる iPhoneアプリ開発の教科書 【Swift 2&Xcode 7対応】 (教科書シリーズ)の内容と一致します

*2:この本ではzipAddressになっています。差別化する意味は無いけど、少し変えました。

*3:まだ、コードしてないからエラーは出てますよ。

*4:dataはオプショナル型なんで警告サインが出ますが無視します

*5:thorowsはSwift2から実装されたエラー・ハンドリングのキーワードです。