Swift Playgroundsで学ぶiOSプログラミング

階層データを深堀するプログラミング

文●柴田文彦 編集●吉田ヒロ

2017年04月17日 17時00分

タイムゾーン配列とその中の項目に関する情報を表示する

 まずは、TimeZone配列の内容を、1つずつテーブルビューの項目として表示するところから始めましょう。これは以前に示したものとほとんど同じです。最初に、テーブルビューの表示に関する部分のコードを示します。

TimeZone配列に含まれるタイムゾーンの名前をそのままテーブルビューの項目名として表示するためのコードです

 これはregionNamesという配列の内容をそのまま表示するものです。この配列の中身は、ビューコントローラーのviewDidLoad()メソッドの中で、dataArrayというもう1つの配列からコピーしています。この図には見えていませんが、このdataArrayには、プログラムの後ろのほうで、ビューコントローラーを作成した直後にTimeZoneクラスのプロパティであるknownTimeZoneIdentifiers配列の内容を設定しています。

 なぜこのような回りくどいことをするのか。1つは、ビューコントローラーオブジェクトに外部から配列を設定することで、表示する配列の内容を後から変更するのが容易になります。もう1つ、ビューコントローラーの中でdataArrayからregionNamesに内容をコピーしているのは、あとで追加するプログラムの中では、regionNamesの内容を改変していくことになるので、元のデータをとっておくためです。ただし、これは必須ではないので、データをとっておく必要がなければdataArrayは省いてもかまいません。

 次にテーブルビューの項目が選択された際に反応するdidSelectRowAtのメソッドの内容を見てみましょう。

タイムゾーンの名前がタップされ、選択された場合の処理です。一般的なビューコントローラーを用意し、そのビューにタイムゾーンの情報を表示します

 なんだか長くなっていますが、基本的な動作は前回示したものと変わりません。行が選択されると、新しいビューコントローラーオブジェクトを作成し、その中のビューに表示したいものを配置して、ナビゲーションコントローラーにプッシュするという流れになっています。

 今回のポイントはTimeZoneクラスを使って、以下のように指定したタイムゾーンの情報を取り出していることです。

let aZone = TimeZone(identifier: regionNames[indexPath.row])

 この際にidentifierとして指定しているのは、テーブルビューのセルに表示しているタイムゾーンの名前そのものです。

 こうして取り出したタイムゾーンオブジェクトであるaZoneからは、まずlocalizedName()メソッドを使ってタイムゾーンの名前を日本語で取り出し、さらにabbreviation()メソッドによってタイムゾーンの時差情報を取り出しています。それらを適当な位置とフォントを指定してUILabelクラスのオブジェクトとしてビューに配置します。このUILabelオブジェクトは、iOSアプリが画面に文字列を表示する際に利用するUIKitの基本的なコントロールの1つです。

 このプログラムを動かすと、まず「Time Zone」というタイトルのナビゲーションバーの下にタイムゾーンの名前の一覧表がテーブルビューとして表示されます。

プログラムを動かすと、タイムゾーンの名前の一覧がテーブルビューに表示されます

 この中から、どれかのタイムゾーンの行をタップするとそのタイムゾーンの名前と時差の情報を表示する画面に切り替わります。この際、ナビゲーションバーには選んだタイムゾーンの名前も表示されています。

前の画面で選んだタイムゾーンの情報を、ナビゲーション機能が管理する別画面で表示します。ナビゲーションバーの「< Time Zone」をタップすれば、元のテーブルビュー画面に戻ります

 このプログラムでも、選んだタイムゾーンの情報を知るという目的は果たせます。しかしタイムゾーンの項目が434個もあるため、目的のタイムゾーンを選ぶのはひと苦労です。その問題を解消するには、テーブルビューを階層的に表示すればいいのです。

同じレベルの項目を1つのテーブルにまとめて階層的に表示するテーブルビュー

 次に示すプログラムでは、上の例でテーブルビューの項目として表示したタイムゾーンの名前を「/」で分割して階層的なデータに変換し、変換後のデータを使ってテーブルビューを階層的に表示します。

 最上位の階層は大陸レベルの名前になりますが、そこで例えば「America」を選んだ場合は、元の名前の先頭が「America」で始まっていた項目の2番目の階層を表す名前を新しいテーブルビューに表示します。上の例の表示を見ればわかりますが、階層の深さは揃っていません。それより下に階層のない項目を選んだ場合にはその時点でタイムゾーンの情報を表示します。下に階層のある項目を選んだ場合には、その下の階層にある名前を展開するテーブルビューを表示します。その際、テーブルビューの項目の下にさらに下の階層があるかどうかを明示するため、下の階層がある項目の右端には「>」のようなマークを表示することにします。

 このプログラムの動きを細かく日本語で説明すると、かなり長くなるだけでなく、かえってわかりにくくなってしまいそうです。そこで、プログラムをいくつかの部分に分けて示しながら要点だけを説明することにします。意味を考えながらコードそのものを読んでみてください。

 まずビューコントローラーの先頭部分では、タイムゾーンの名前の上位部分を記録しておく変数trailと、表示している項目の下位の階層を記録する配列areaNamesを定義しています。

class PGTVController: UITableViewController {
  var dataArray = [String]()
  var trail = String()
  var regionNames = [String]()
  var areaNames = [[String]]()

 ビューコントローラーのviewDidLoad()メソッドでは、与えられたTimeZoneの名前の配列を処理し、「/」で区切った先頭の名前(これがテーブルビューに表示されます)と、残りの部分に分割します。先頭はregionNames配列に格納し、残りの部分は先頭の名前に対応するグループごとareaNames配列に格納します。この部分は、以前にタイムゾーンをセクションごとに分けて表示した際の処理と似ています。違いは、残りの部分を再び「/」で区切って、下の階層の処理に受け継ぐことです。

override func viewDidLoad() {
  var newRegion = ""
  var regionIndex = -1
  for aZone in dataArray {
    var tZonePath = aZone.components(separatedBy: "/")
    if tZonePath[0] != newRegion {
      newRegion = tZonePath[0]
      regionNames.append(newRegion)
      regionIndex += 1
      areaNames.append([])
    }
    var areaName = ""
    for i in 1 ..< tZonePath.count {
      areaName += tZonePath[i]
      if i != tZonePath.count - 1 {
        areaName += "/"
      }
    }
    areaNames[regionIndex].append(areaName)
  }
}

 テーブルビューのcellForRowAtメソッドでは、基本的にregionNames配列の内容を個々のセルに表示します。その際、その項目の下に続く階層がある場合(対応するareaNames配列に有効な要素が含まれている場合)には、そのセルの右側に「>」マーク(.disclosureIndicator)を表示します。

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = UITableViewCell()
  cell.textLabel?.text = regionNames[indexPath.row]
  if areaNames[indexPath.row] != [""] {
    cell.accessoryType = .disclosureIndicator
  }
  return cell
}

 テーブルビューのdidSelectRowAtメソッドは、大きく2つの部分に分かれています。もし選ばれた項目の下の階層がない場合は、上に示した例と同じように、そのタイムゾーンの情報を別のビューコントローラーに表示します。それが前半です。

 後半は、さらに下に階層が続く場合の処理です。そこでは、なんと自分自身のクラスであるPGTVControllerのオブジェクトを作成し、そこに下に続く階層のデータをセットしてから、ナビゲーション機能を使ってその新しいテーブルビューコントローラーに表示を切り替えています。こうすることで、下の階層が続く限りずっとテーブルビューで対応できるのです。

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  tableView.deselectRow(at: indexPath, animated: false)
  if areaNames[indexPath.row] == [""] {
    let aZone = TimeZone(identifier: self.trail + regionNames[indexPath.row])
    let dvc = UIViewController()
    let nBounds = self.navigationController?.visibleViewController?.view.bounds
    dvc.view = UIView(frame: nBounds!)
    dvc.title = regionNames[indexPath.row]
    var nameLabel = UILabel(frame: CGRect(x: 0, y: 70, width: dvc.view.bounds.width, height: 60))
    nameLabel.text = aZone?.localizedName(for: .standard, locale: Locale.current)
    nameLabel.font = UIFont.systemFont(ofSize: 30.0)
    nameLabel.textAlignment = .center
    var abbLabel = UILabel(frame: CGRect(x: 0, y: 120, width: dvc.view.bounds.width, height: 60))
    abbLabel.textAlignment = .center
    abbLabel.text = aZone?.abbreviation()
    abbLabel.font = UIFont.systemFont(ofSize: 24.0)
    dvc.view.backgroundColor = .lightGray
    dvc.view.addSubview(nameLabel)
    dvc.view.addSubview(abbLabel)
    dvc.view.contentMode = .scaleToFill
    self.navigationController?.pushViewController(dvc, animated: true)
  } else {
    let ntvc = PGTVController()
    ntvc.title = regionNames[indexPath.row]
    ntvc.trail = self.trail + regionNames[indexPath.row] + "/"
    ntvc.dataArray = areaNames[indexPath.row]
    self.navigationController?.pushViewController(ntvc, animated: true)
  }
}

 プログラムの最後の部分(最初に実行される)は、上に示した例とほとんど同じです。1行だけ加えたのは、PGTVControllerのオブジェクトのtrailプロパティに空の文字列を代入する文です。これによって、タイムゾーンの選択の軌跡を記録する変数を初期化しています。

let pgtvc = PGTVController()
var pgnvc = UINavigationController(rootViewController: pgtvc)
pgtvc.title = "Time Zone"
pgtvc.trail = ""
pgtvc.dataArray = TimeZone.knownTimeZoneIdentifiers
PlaygroundPage.current.liveView = pgnvc

 このプログラムを実行すると、まず表示されるのは大陸レベルの領域名のテーブルビューです。

プログラムを実行すると、地球上の大陸レベルの領域名が、単独でテーブルビューの項目として表示されます。下に階層の続く項目の右端には「>」マークが表示されています

 この中で「GMT」だけは下に続く階層がないので、右端に「>」マークがありません。

 ここで、例えば「America」をタップすると、次にアメリカ大陸に含まれる領域名のテーブルビューが表示されます。

前のテーブルビューで「America」を選ぶと表示されるテーブルビューです。アメリカ大陸に含まれる国レベルの領域名のテーブルビューが表示されます

 この中で、下に階層が続くのは「Argentina」(アルゼンチン)だけです。それをタップしてみると、アルゼンチンの中の領域のテーブルビューに切り替わります。

前のテーブルビューで「Argentina」を選ぶと表示されるテーブルビューです。アルゼンチンは面積が大きいので、複数のタイムゾーンが含まれています

 この下に続く階層は1つもないので、「>」 マークが表示されているセルはありません。ここで例えば「San_Juan」をタップしてみると、「アルゼンチン標準時/GMT-3」のように表示されます。

前のテーブルビューで「San_Juan」を選ぶと表示されるタイムゾーン情報です。タイムゾーンの名前と時差情報を表示しています

 今回はプログラムが長くなり、入力も大変なので、ソースコードを含むプレイグラウンドファイルをダウンロードできるようにしました。ダウンロードしたファイルを展開してから各自のiCloudドライブにアップし、Swift Playgroundsの「マイプレイグラウンド」ページの左上にある「+」をタップして「iCloud Drive」を選択して目的のファイルを開いてください。

ダウンロードはこちらをクリック。

次回の予定

 今回は、TimeZoneの名前の配列を階層的なデータとみなして、多段階のテーブルビューとして展開する例を示しました。このような機能は、TimeZone配列だけでなく、同じような構造を持った別の配列にも応用できるはずです。次回は、テーブルビューの使い方の締めくくりとして編集機能を取り上げる予定です。それによって、簡単なTo Doリストのプログラムを作ります。

mobileASCII.jp TOPページへ

mobile ASCII

Access Rankingアクセスランキング