Swiftで遊ぼう! on Hatena

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

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

2017年6月12日:Swift 4β版に暫定的に対応*1

JSONとは?

JavaScript Object Notationの頭文字です。なんとJavaScriptで取り決められているデータ構造です。Webサービスで情報のやりとりをするときに可読性が優れていることで標準になっていますが、ユルユルな文字列のみで構成されているデータ構造のため厳格なタイプチェック機能のあるSwiftとデータをやりとりするのに苦労させられます。Swift 3まで煩雑なコーディングを強いられていましたが、Swift 4から自動化が組み込まれ楽になることが期待されています。

まずJSONの表記法を説明します。

[ 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データを扱うクラスがSwift 4から新しく用意されます。それが「JSONDecoder」です。Swift 3までは、デベロッパーがSwift以外のデータ構造を解析してSwiftのデータ構造に取りこむ「パース(Parse)」という作業をしなければならなかったので、NSJSONSerialization*2クラスを使ってSwiftの言語体系に取りこんでから配列と辞書にキャストさせる必要がありました。

この作業がSwift 4で自動化されたんです!これは初心者の私には非常に便利な機能です。用意しなければならない事が1つあります。JSONデータ構造に一致した「Struct」を用意して、新しく「Codableプロトコールに準拠する必要があります。またJSONで一般的な記述法「snake case*3」をSwiftの「camel case」に変更するために「CodingKeyプロトコールが用意されています。

例えば次のようなJSON型のデータの場合(WWDC2017から)

// JSONデータ
{
  "url": "https://api.github.com/.../6dcb09",
  "author": {
    "name": "Monalisa Obtocat",
    "email": "support@github.com",
    "date": "2017-06-12T16:00:233"
  },
  "message": "Fix all the bugs",
  "comment_count": 0,
}

//Swift 4で用意する構造体は次のようになります。
struct Commit: Codable {
  let url: URL
  struct Author: Codable {
    let name: String
    let email: String
    let date: Date
  }
  let author: Author
  let message: String
  let commentCount: Int

  private enum CodingKeys: String, CodingKey {
    case url
    case author
    case message
    case commnetCount = "commnet_count"
}

データ構造も壊れているかもしれません。このように外部とデータをやり取りするクラスメソッドの場合、Swift 2からNSErrorタイプのエラーを投げる(throw)仕組みが実装されエラーハンドリングの機能が加わりました。Swift 3でも改訂は続き、多くのクラスメソッドで実装が進みました。利用時に必ずerrorを受け取る仕組み(try do-catch)が必要になっています。Swift 4から利用できる「JsonDecoder」のエラー処理は更に高度化してデバック作業をしやすくしています。次のデモの中で説明します。

ZipToAddressチュートリアルアプリ(Swift 4β対応)

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

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

  1. 郵便番号の入力を受け付けるUITextFieldオブジェクトと結果を表示するUILabelオブジェクトの用意
  2. 検索を実行するためのUIButtonオブジェクトを用意してWebAPIを送信する
  3. 外部から戻ってくるデータはJSONデータなので自分で用意したSwiftのstruct型にJsonDecoderを使ってデコードします。

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

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値に辞書型を持った入れ子状態です。

// 検索が成功したとき
{
  "code": "200,
    "data": {
      "pref": "愛知県"
      "address": "名古屋市中村区上米野町"
      "city": "名古屋市中村区"
      "town": "上米野町"
      "fullAddress": "愛知県名古屋市中村区上米野町"
}

// 検索が失敗したとき
{
  "code": "404",
  "message": "Address not found."

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

ZipToAddressプロジェクト

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

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

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

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型のアドレスを使ってURLクラスを生成します。しかし、この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ファイルに存在するからです*5

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

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

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

そして次に重要なステップがあります。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や4では利用するData型ではサイズ(109 bytes)しか分かりません。これを見ただけでもセキュリティが向上しているようです。

tapSearchのコーディングをまとめると次のようになります。DisptchQueueや[unowned self]が分からない人はSwiftで遊ぼう! - 302 - マルチスレッド(まとめ) - Swiftで遊ぼう! on HatenaSwiftで遊ぼう! - 869 - ARCとメモリーマネージメント - Swiftで遊ぼう! on Hatenaを理解して下さい。基本です。

@IBAction func tapSearch(_ sender: UIButton) {
    guard let zipText = zipTextField.text else {
        // nilになり何の処理もされません。
        return
    }
        
    let urlString = "http://api.zipaddress.net/?zipcode=\(zipText)"
        
    if let url = URL(string: urlString) {
        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config)
        DispatchQueue.global(qos: .userInitiated).async { 
            [unowned self] in
            let task = session.dataTask(with: url, 
                           completionHandler: self.onGetAddress)
            task.resume()
        }
    }
}

これでダウンロードした辞書データは、JSONデータなので、onGetAddressメソッド(まだ実装していません)を使ってデコードしなければなりません。

まずJSONデータを受け取るSwiftのstruct型を用意する必要があるので、Xcode 9βの「File」->「New」->「File...」を選択して、「Swift File」を選択します。名前は「AddressData.swift」にします。

そしてJSONデータ構造と同等のstructを用意します。JSONのKey値とSwiftのプロパティ名は一致させる必要があります。一致できない場合はCodingKeyプロトコールを使用します。辞書検索サイトからダウンロードするJSONデータに会わせて次のコードを用意します。

import Foundation

struct AddressData: Codable {
    let code: Int
    struct Address: Codable {
        let pref: String
        let city: String
        let town: String
        let fullAddress: String
    }
    let data: Address?
    let message: String?
}

辞書検索サイトでは、2つのパターンが確認できます。codeは必ずはき出されるのでInt型で受けます。データがあればdata値に受け取って、エラーになればmessageを受け取るため両者をオプショナル値にしました。

これだけです。Swift 4からJSONデータをパース(解析)する必要がないんです。後は自動で割り振ってくれるんです。

まず、ViewController.swiftファイルのPropertiesの「@IBOutlet weak var addressLabel: UILabel!」の下に次のプロパティを加えます。

var addressData: AddressData?

そして、「 onGetAddress()」メソッドを実装します。

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

    guard let jsonData = data else {
        print("Error downloading!")
        return
    }
...

次は、自動的にデコードするためのデコーダーの初期化ステップです。

...
    let decoder = JSONDecoder()
...

そして、デコーダーを使用すればいいだけです。超簡単になりました!

...
    do {
        addressData = try decoder.decode(AddressData.self, 
                                             from: jsonData)
    } catch DecodingError.keyNotFound(let key, let context){
        print("Missing Key: \(key)")
        print("Debug descriptin: \(context.debugDescription)")
    } catch DecodingError.valueNotFound(let type, let context) {
        print("\(type) and \(context.debugDescription)")
    }catch DecodingError.typeMismatch(let type, let context) {
        print("\(type) and \(context.debugDescription)")
    } catch {
        print("downloading error: \(error.localizedDescription)")
    }
...

エラーを細かく見分けることができます。これはSwift 4で拡張された機能です。

...
    if addressData?.code != 200 {
        DispatchQueue.main.async {
            guard let jsonCode = self.addressData?.code else {
                return
            }
            guard let errorMessage = self.addressData?.message else {
                return
            }
            print("code: \(String(describing: jsonCode))\n 
                   message: \(errorMessage)")
        }
    }
        
    guard let pref = addressData?.data?.pref else {
        return
    }
        
    DispatchQueue.main.async {
        self.prefLabel.text = pref
    }
        
    guard let city = addressData?.data?.city else {
        return
    }
        
    DispatchQueue.main.async {
        self.cityLabel.text = city
    }
        
    guard let town = addressData?.data?.town else {
        return
    }
        
    DispatchQueue.main.async {
        self.townLabel.text = town
    }
        
    guard let address = addressData?.data?.fullAddress else {
        return
    }
        
    DispatchQueue.main.async {
        self.addressLabel.text = address
    }
        
}

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

f:id:yataiblue:20170408234318j:plain

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

*2:Serialization(シリアライゼーション)は、ここで説明しているようにバイナリ化するという意味もあります。

*3:プロパティ名に「_」を加えて単語を繋ぐ記述法

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

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

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