バスの話です。
アイディア
ローカルな話題で申し訳ないですけど、私はよく調布駅から三鷹までバスに乗ります。三鷹に美味しい店があるんです。通勤時間帯の混雑したバスは嫌ですけど、休日の、空いている時間帯のバスはのんびりしていて好きです。
調布駅北口の、パルコの前の乗り場で三鷹行きのバスを待っていると、目の前を「三鷹行き」のバスが通り過ぎていくことがあります。えっなんで? と悔しい思いを何度かしました。どういうことかといいますと、経路が少し違う3つの三鷹行きの路線が、それぞれ別の乗り場から出ているんですね。これは慣れないと分かりづらいです。
以下にあるのが、12番乗り場から出る三鷹行きの時刻表の例です。数字は出発時刻(分)を示します。これは日曜の10時台の例です。
06,20,33,47,59
ちなみにこれは鷹56という路線名で、神代植物公園を経由します。
次に11番乗り場から出るやつ。(同じく日曜の10時台)
00,20,39,58
これは鷹66という路線名で、布田から三鷹通り、航研前を経由します。
最後に14番乗り場から出るやつ。(同じく日曜の10時台)
22
これは鷹51という路線名で、電通大、天文台を経由します。
地図で見てもらうと分かりやすいのですが、調布周辺から北に向かう3本の道路、三鷹通り、武蔵境通り、天文台通り沿いを通る経路にそれぞれ対応しています。三鷹駅までの所要時間はそれぞれ異なりますが、外の景色を眺めながらぼーっとしたい私にとっては、少々の所要時間の差など気になりません。
3つの時刻表を混ぜるとこんな感じになります(無印が鷹56、丸括弧内は鷹66、鉤括弧内は鷹51)
00,06,20,(20),[22],33,(39),47,(58),59
1時間にこのくらい本数があれば、便利な感じがしますね。欲を言えば、もう少し間隔を均等にして貰えばありがたいのですが、このあたりはバス会社の都合なのでしょうから文句は言えません。
調布駅北口のバス乗り場に、高級ホテルにいるような「コンシエルジュ」みたいな人が常駐していて「次の三鷹駅行きはどこから?」と訊くと「10:33に12番乗り場から出ます。あと1分少々です。お客様、お急ぎくださいませ」とか答えてくれればいいのに、と思うことがあります。
っていうか、そういうアプリがあればいいんじゃないの? どこかにある? ない? じゃあ作っちゃう?
ということで、以前から興味のあったSwiftで自分用のアプリを作ってみることにしました。
アプリを起動すると「次の三鷹行きは?」というボタンが表示されて、タップすると「〇番乗り場からxx:xxに出るよ」と表示するだけ。シンプルそのものです。
出来上がりのイメージはこんな感じ。余白が目立ちますね。付け足そうと思えばいくらでも付け足すことは出来そうですが、とりあえず必要最小限の機能に絞ることにします。
*最近、シンプルなUIに関する@monsoonTropicalBirdさんの記事を読み、感銘を受けました。(この記事の冒頭にある「間違ったUI」のイメージがすっごい笑える。日本のお役所感覚の人たちが作るUIってほんとあんな感じなんですよ)
*余談ですけど、昨今の円安の影響で、アップルが自社製品の値上げを発表しました。アップル製品は今後「高級な外国製品」ということになって、日本の庶民の手に届かなくなってしまうのではないか、と不安になります。1984年、初代Macがアメリカで$2500で発売された時、日本での販売価格は69万8千円だったそうです。この時代にMacを買った人たちは尊敬と、たぶん少しのやっかみを込めて「Macオーナー」と呼ばれたらしい。
初めてのMacを買おうかどうしようか迷っている人、今後円がどうなるか責任は持てませんけど、早めに決断して買ってしまうことをおすすめします。Macがあなたに与えてくれるメリットと幸福を、1日でも早く享受すべきです。
プロジェクトを作ってみる
ということで、私にとって初のSwiftアプリです。備忘録として残しておきます。
もう少し詳しく仕様を考えてみます。その前にXcodeで新しいプロジェクトを作りましょう。最初に枠を作って、あれこれ動かしながら考えるのが私のやり方です。これにはメリットがあって、アプリの動作をイメージしやすいということの他に、思わぬ落とし穴を、あらかじめ(開発が後戻りできないくらい進んでしまう前に)見つけることができます。
XcodeをインストールしたMacを用意します。私の環境は以下の通りです。
MacBook Air(M1 2020)
MacOS Monterey 12.4
Xcode 13.4.1 (13F100)
実機で動作させるためには、Xcodeをインストールする時に使ったAppleIDと同じIDのiOSデバイスが必要です。
Xcodeを起動して、Welcome画面からCreate a new Xcode projectをクリックします。
次の画面で、プラットフォームにiOS、ApplicationでAppを選択し、Nextボタンをクリックします。
その次の画面で、以下のようにプロジェクトの情報を入力します。
Product Name: プロジェクトの名前をつけます。このプロジェクト名のフォルダー以下にソースファイル他プロジェクトに必要なファイルが納められます。また下記のBundle Identifierにも使われます。iPhoneのホーム画面に表示するアプリ名は後で設定できます。
Team: 今のところNoneで構いません。
*実機にアプリを転送するにはアカウントを指定する必要がありますが、これは後でやります。
Organization Identifier: ここは世界中で一意の組織識別名を入れることになっていますが、個人のメールアドレスでかまいません。メールアドレスが"myname@hogehoge.jp"ならここに入れる文字列は"hogehoge.jp.myname"のようにします。
Bundle Identifier: ここには上記のProduct NameとOrganization Identifierを足した文字列が表示されます。これはAppストアでアプリを公開する時に使われます。
Interface: SwiftUIを選択します。
language: Swiftを選択します。
Use Core DataとInclude Testsのチェックボックスはとりあえずチェックしないままにしておきます。
Nextボタンをクリックすると、プロジェクトをどこに保存するか聞いてきますので、保存したいフォルダーを選択しCreateボタンをクリックします。選択したフォルダーの下にプロジェクトのフォルダーが作られます。
ビルド/実行してみる
Xcodeが起動された状態では、画面の左にプロジェクトナビゲーターがあり、ContentView.swiftファイルが選択された状態で表示されていると思います。
ContentView.swiftのコードはこんな感じだと思います。
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
画面の左上にあるプレイボタンをクリックするとビルドが行われ、Canvasもしくはシミュレータが現れ、デバッグセッションが始まります。(画面が表示されるまで時間がかかるかもしれませんが、辛抱強く待ちましょう)
上の画像のMy Mac(Designed for iPad)のあたりをクリックするとドロップダウンリストが現れ、シミュレータを変更できます。一覧にないシミュレータはAdd Additional Simulators...から追加できます。
画面が現れ、中央に"Hello world!"が表示されたら成功です。
プレイボタンの横に現れた四角いストップボタンをクリックするとデバッグセッションが終了します。
ContentViewのコードを以下のようにしてしてみましょう。
struct ContentView: View {
var body: some View {
VStack {
Button( action: {
print("---ボタンが押された時の処理---")
}){
// 初期画面(ボタン)
Text("次の三鷹行きは?").font(.largeTitle)
}
}.onAppear{
print("---起動された時の処理---")
}
}
}
うまくいくとシミュレータ画面の中央に"次の三鷹行きは?”ボタンが表示されます。
Xcodeの下のほうにあるDEBUGウインドウに注目してください。起動した時に"---起動された時の処理---"が表示されるはずです。(他にもごちゃごちゃと表示されますが、今のところは気にしないことにします)
シミュレータが現れたら"次の三鷹行きは?"をクリックします。クリックするたびに"---ボタンが押された時の処理---"がデバッグウインドウに表示されると思います。
時刻表データを読み込むのは、アプリが起動された時に1回だけやればいい処理なので、上記の"---起動された時の処理---"の部分でやり、ボタンが押された時の処理は、"---ボタンが押された時の処理--"の部分でやります。
超ざっくりですが、アプリの構造が決まりました。
- 2022/08/22追記
.onAppearに起動時に1回だけやる処理を記述するのは、Viewが複数になった場合に問題が起きる可能性があります。以下の記事にそのあたりのことの説明を書きました。
データクラス
時刻表データを扱うクラスを作ります。Xcodeのナビゲーションウインドウで右クリックするとメニューが出てきますのでNewFile…を選択します。iOSプラットフォームのSwiftファイルを選択し、ファイル名をJikokuData.swiftとして保存します。
時刻表データの読み込みくらいならContentView.swiftの中にちゃちゃっと書いちゃえばいいのに、なんでわざわざ別ファイルを作るの? と思った人のためにちょっとだけ説明します。
なぜファイルを分けるのか? それはViewに何でもかんでも詰め込んでしまうと、アプリの規模が大きくなった時に、こんがらがって訳がわからなくなってしまうからです。
そこで、ContentViewには画面表示やユーザーからの指示に関する部分だけ記述し(View/Controller)、データの扱い(Model)とは別にしよう、きっとその方が後で見た時にわかりやすいよね、というのが、データを扱う部分とViewを別にする理由です。その点については「MVC デザインパターン」とかをキーワードにして検索すると、オブジェクト指向デザインパターンに関する文献が星の数ほどヒットすると思いますので探してみてください。
*デザインパターンというのは、単なるルールなのですが、大規模なプロジェクトでやる場合、隅から隅まで気を配れる優秀なリーダーがいないと、ルールが破綻して悲惨なことになります。結果、珍奇/複雑/怪奇なコードの山を築いた挙句、泥沼にはまって抜け出せなくなったプロジェクトをいくつか見たことがあります。
ではJikokuData.swiftにコードを入力してみましょう。メソッドの中身はいまのところ空です。先ずは構造を先に決めて、上手くいくかどうか確認します。
import Foundation
class JikokuData
{
// 結果文字列
let ansArray: [String] = ["乗り場12", "12:34"]
//
// 時刻表データを読み込む
//
func LoadJikokuData() {
print("***LoadJikokuData Start.")
// ここで時刻データの読み込みをやる
print("***LoadJikokuData End.")
}
//
// 時刻表をサーチする
//
func SearchJikokuData()->[String] {
print("***SearchJikokuData Start***")
// いまはとりあえずダミー文字列を返すだけ
print("***SearchJikokuData End***")
return ansArray
}
}
ContentView.swiftに時刻表データを扱う部分のコードを入力します。「起動時の処理」の部分に時刻表データを読み出すメソッドを、「ボタンが押された時の処理」で、時刻表をサーチして結果を返すメソッドを呼び出しています。
struct ContentView: View {
@State var JikokuDataList = JikokuData()
@State var resultText1 = ""
@State var resultText2 = ""
var body: some View {
VStack {
Button( action: {
print("---ボタンが押された時の処理---")
let tmpstr = JikokuDataList.SearchJikokuData()
resultText1 = String(format: "%@から",tmpstr[0])
resultText2 = String(format: "%@に出るよ",tmpstr[1])
}){
// 初期画面(ボタン)
Text("次の三鷹行きは?").font(.largeTitle)
}
// 初期画面(結果表示エリア)
Text(resultText1)
.font(.largeTitle)
.frame(width:400)
Text(resultText2)
.font(.largeTitle)
.frame(width:400)
}.onAppear{
print("---起動された時の処理---")
JikokuDataList.LoadJikokuData()
}
}
}
ビルドして実行してみましょう。デバッグウインドウを見ると、起動時にLoadメソッドが実行され、ボタンクリック時にSearchメソッドが実行されるのが分かると思います。
今日はなんの日?
JikokuDataのSearchメソッドには今日がなんの日かの判定が必要です。時刻表は、平日用、土曜日用、日祝用の3種類あるので、どの時刻表データを使うべきか判断しなければなりません。それで今日は何の日か判定する必要があるのです。
それならLoadする時にその日が何の日か判断して、必要な時刻表だけLoadしてやればいいんじゃないの、と思うかもしれませんが、それだとダメなんです。それについてはiOSアプリのライフサイクルについて考えてみる必要があります。
ライフサイクル?
iOSのアプリって、ホームボタンをタップしても終了するわけじゃなくて、バックグラウンドに回されてメモリの中にしつこく残ってるんですよ。iPhoneのホームボタンをダブルタップして、メモリの中にいるアプリの一覧を表示すると、こいつまだいるの? っていうアプリがあったりしますよね? そいつらがメモリから消えるのはOSがメモリ不足で「いらね」と思ってKillするか、ユーザーが意識的にメモリ内のアプリ一覧から追いだすか、OSを再起動する時です。ホーム画面からアイコンをタップした時に、メモリの中にいたやつがぬっと顔を出す場合もあるんですね。つまり、金曜日に起動したやつの「次の三鷹行きは?」ボタンがタップされるのは、もしかしたら、土曜日かもしれない、というわけです。これはまずいですよね。
というわけで、起動時に全ての時刻表を読み込んで、ボタンをタップされた時にどの時刻表を使うか決める、という流れにする必要があるわけです。
日本の祝日?
今年の祝日一覧を手に入れて、csvファイルにしてこれを読み込むことにします。祝日の一覧は内閣府(!)のホームページから入手可能です。他にもネット上で今日が祝日かどうか判定してくれるWeb APIを公開されている方などもいらっしゃいます。探してみてください。
csvファイルは以下のようなフォーマットにします。このファイルをholidays.csvというファイル名をつけてプロジェクトのフォルダーに保存します。
2022/01/01,元日
2022/01/10,成人の日
2022/02/11,建国記念の日
.
.
.
FinderからXcodeのナビゲーターウインドウにドロップします。ダイアログが現れますのでOKとしてください。
CSVファイルを読み込む
JikokuDataクラスで使う、csv形式のファイルを読み込む機能を作ります。JikokuData.swiftファイルを作った時の要領でCSVData.swiftファイルを作ります。
import Foundation
// CSVデータを扱うクラス
class CSVData {
// 指定されたファイル名のcsvファイルをリソースから読み込み
// 文字列の配列に行ごとに格納して返す
// (ファイル名には拡張子(.csv)を除いた部分を与えること)
func LoadCSVData(fname: String)->[String] {
//CSVファイルを格納するための配列を作成
var csvArray:[String] = []
// アプリのリソースから
let csvBundle = Bundle.main.path(forResource: fname, ofType: "csv")
do {
//csvBundleのパスを読み込み、UTF8に文字コード変換して、NSStringに格納
let csvData = try String(contentsOfFile: csvBundle!,encoding: String.Encoding.utf8)
//改行コードが"\r"で行なわれている場合は"\n"に変更する
let lineChange = csvData.replacingOccurrences(of: "\r", with: "\n")
//"\n"の改行コードで区切って、配列csvArrayに格納する
csvArray = lineChange.components(separatedBy: "\n")
}
catch {
print("エラー")
}
return csvArray
}
}
ここではリソースファイル内のテキストファイルを読み込むだけで、カンマ区切りのデータを取り出すのは、呼び出すクラスの方でやります。
祝日一覧データを扱うクラスを作ります。HolidayData.swiftをナビゲーターウインドウで作り、以下のようなコードを追加します。
import Foundation
// 祝日ひとつ分を扱うクラス
class HData: NSObject {
var name : String! // ex.元旦(祝日の名前)
var date : String! // ex.2022/01/01(月日)
}
// 祝日一覧を扱うクラス
class HolidayData {
// 祝日一覧を格納する配列
var holidayArray : [HData] = []
// CSVデータを扱うクラス
var csv = CSVData()
// 祝日一覧を読み込む
func LoadHolidays() {
print("***LoadHolidays Start.")
let filename: String = "holidays"
//CSVファイルを格納するための配列
var csvArray:[String] = []
//リソースからCSVファイルを読み込む
csvArray = csv.LoadCSVData(fname: filename)
// csvArrayに入れたテキストを祝日一覧配列に入れる
for aLine in csvArray {
let holiday = aLine.components(separatedBy: ",")
if (holiday[0] != "") {
let data1 = HData()
data1.date = (holiday[0])// 日付
data1.name = (holiday[1])// 祝日の名前
let tmp = String(format: "%@:%@",data1.date,data1.name)
print(tmp)
holidayArray.append(data1)
}
}
print("***LoadHolidays End.")
}
// 指定された日が祝日かどうか判定する
func IsitHoliday(today: Date) -> Bool {
print("***IsitHoliday Start.")
// today(UTC)からyyyy/MM/dd形式の文字列を得る
// (こうするとUTCが+9(JST)される
let cur = Calendar.current
let year = cur.component(.year, from: today)
let month = cur.component(.month, from: today)
let day = cur.component(.day, from: today)
let todaystr = String(format: "%d/%02d/%02d",year,month,day)
for aday in holidayArray {
if aday.date == todaystr {
let tmp = String(format: "今日は%@:%@(祝)",aday.date,aday.name)
print(tmp)
return true
}
}
print("***IsitHoliday End.")
return false
}
}
祝日一覧データを読み込む機能と、今日が祝日かどうか判定する機能があります。
時刻表データ
時刻表データは以下のようなCSV形式で作ります。これは鷹56の平日用の時刻表です。このようなCSVファイルを鷹56、鷹66、鷹51の3つの路線用に、それぞれ平日、土曜、日祝用に作ります。合計9つのファイルが出来ると思います。
6,23,36,47,55
7,02,10,19,29,36,44,50,58
8,06,12,19,25,32,39,46,55
.
.
.
最初のカラムが時を示し、以降行末までが分のデータを示します。
1行目を例にすると、6:23、6:36、6:47、6:55のデータがあります。
これらのファイルにはそれぞれ以下のような規則でファイル名をつけます。
路線名_出発地_目的地_乗り場_(平日/土曜/日祝).csv
全ての時刻表データをナビゲーターウインドウにドロップして、リソースとして取り込みます。
JikokuDataクラス
JikokuData.swiftにコードを追加して、以下のようにします。
import Foundation
// 時刻ひとつ分のデータ
class TData: NSObject {
var lno: Int! // 路線番号 0:鷹56 1:鷹66, 2:鷹51
var tm : Int! // 出発時刻 ex.632(6:32)
}
class JikokuData
{
// CSVデータを扱うクラス
var csv = CSVData()
// 時刻表データ
public var tDataArray : [[TData]] = [[],[],[]]
// 休日一覧データ
var holidays = HolidayData()
// 路線名_出発地_目的地_乗り場_(平日/土曜/日祝)
let lineNameArray: [String] = [
"鷹56_調布駅北口_三鷹駅_乗り場12_", //0
"鷹66_調布駅北口_三鷹駅_乗り場11_", //1
"鷹51_調布駅北口_三鷹駅_乗り場14_" //2
]
let DayNameArray: [String] = [
"平日", // 0
"土曜", // 1
"日祝", // 2
]
// [0]:平日 [1]:土曜 [2]:日祝
enum DayIndex: Int {
case Weekday = 0
case Saturday
case SundayOrHoliday
case count // 要素数
}
// 時刻表データを読み込む
public func LoadJikokuData() {
print("***LoadJikokuData Start.")
// 祝日一覧を読み込む
holidays.LoadHolidays()
// 時刻表CSVファイル名
var filename: String
//CSVファイルの中身を格納するための配列
var csvArray:[String] = []
// 何の日かによるループ 0:平日 1:土曜 2:日祝
for dayindex in 0 ..< DayIndex.count.rawValue {
// 路線の数の分ループ 0:鷹56 1:鷹66 2:鷹51
for (lcnt,na) in lineNameArray.enumerated() {
// 何の日かによってファイル名を補完
filename = na + DayNameArray[dayindex]
// リソース内の時刻表データを読み込む
csvArray = csv.LoadCSVData(fname: filename)
// CSVの行(=時の数)分ループ
for aLine in csvArray {
let line = aLine.components(separatedBy: ",")
// 1行のデータの例: 6,26,39,48,55 (最初のカラムが時(この場合6時で、残りが分(26分,39分,48分,55分)
// TDataArrayのtmに整数で626,639,648,655のように入れる
var hour = 0
if (line[0] != "") {
// カラムの数分ループ
let itemMax = line.count
for item in 0 ..< itemMax {
//[0]はhourに取っておく
guard item >= 1 else {
hour = Int(line[item])!
continue
}
let data1 = TData() // 1個分のデータ lno:路線番号 tm:時刻
// 路線番号
data1.lno = lcnt
// 時分
data1.tm = hour * 100 + (Int(line[item])!)
tDataArray[dayindex].append(data1)
}
}
}
// 時間でソートする
tDataArray[dayindex].sort(by:{$0.tm < $1.tm})
}
}
print("***LoadJikokuData End.")
}
// 時刻表をサーチする
func SearchJikokuData()->[String] {
print("***SearchJikokuData Start***")
// 結果文字列
var ansArray: [String] = ["乗り場xx", "xx:xx"]
// 何の日(0:平日、1:土曜、2:日祝)
var dayindex : Int = DayIndex.Weekday.rawValue
// 今日(UTC)
let today = Date()
// 今日が祝日一覧に含まれるか
if (holidays.IsitHoliday(today: today)){
// 祝日の場合の処理
dayindex = DayIndex.SundayOrHoliday.rawValue
print("今日は祝日")
} else {
// (2)祝日でなければ日曜か平日か土曜
let weekDay = Calendar.current.component(.weekday, from: today)
enum WeekDay: Int {
case sunday = 1
case monday = 2
case tuesday = 3
case wednesday = 4
case thursday = 5
case friday = 6
case saturday = 7
}
// weekDay --> dayindex変換
switch weekDay {
case WeekDay.sunday.rawValue:
dayindex = DayIndex.SundayOrHoliday.rawValue
print("今日は日曜")
case WeekDay.saturday.rawValue:
dayindex = DayIndex.Saturday.rawValue
print("今日は土曜")
default:
dayindex = DayIndex.Weekday.rawValue
print("今日は平日")
}
}
// 現在時刻(時分)-->Int変換
let cur = Calendar.current
let hour = Int(cur.component(.hour, from: today))
let minute = Int(cur.component(.minute, from: today))
let nowt = hour * 100 + minute
// 現在時刻以降のデータを時刻表の配列から得る
for dt in tDataArray[dayindex] {
if dt.tm >= nowt {
// 結果文字配列をansArrayに入れる
// [0]:路線名 [1]:出発地 [2]:目的地 [3]:乗り場
let tmp = lineNameArray[dt.lno].components(separatedBy: "_")
// 乗り場
ansArray[0] = tmp[3]
let hour = String(format: "%02d",dt.tm/100 )
let minutes = String(format: "%02d",dt.tm%100 )
// 出発時刻
ansArray[1] = hour + ":" + minutes
// 1個見つけたら終了
break
}
}
print("***SearchJikokuData End***")
return ansArray
}
}
デバッグセッションの際中に「この変数の中身を見たいな」と思ったら、ブレイクポイントを指定して、コードを停止させることができます。コードウインドウの左端にある行番号をクリックすると、青色でブレイクポイントが表示されます。ブレイクしたら、ステップ実行したりもできます。Debugメニューにあるので、確認してみてください。
デバッグウインドウの横のウォッチウインドウで変数の中身を確認できます。
3つの路線の時刻表を読み込んだ配列を、時刻でソートしている部分がありますよね。あそこがこのプログラムの「ミソ」です。
// 時間でソートする
tDataArray[dayindex].sort(by:{$0.tm < $1.tm})
ちゃんとソートされているかどうか、ContenViewのコードを修正して、デバッグウインドウに表示させてみましょう。
}.onAppear{
let startDate = Date()// 処理時間計測
print("---起動された時の処理---")
JikokuDataList.LoadJikokuData()
// デバッグ時表示
for i in 0 ..< JikokuData.DayIndex.count.rawValue {
print("Arraysize=\(JikokuDataList.tDataArray[i].count)")
for dt in JikokuDataList.tDataArray[i] {
let tmp = String(format: "lno=%d tm=%04d",dt.lno,dt.tm)
print(tmp)
}
}
let endDate = Date()
print("時刻表読み込み処理時間は " + String(endDate.timeIntervalSince(startDate)))
}
デバッグウインドウに出力された時刻データが、時間順に並んでいることを確認してください。
デバッグセッションが開始されてから、シミュレータに初期画面が表示されるまでの時間が長くなったような気がしたので、処理時間を計測するコードを追加してみました。結果はms単位の処理時間で、気になるほどの時間はかかっていません。ついでにSearchについてもどのくらいの時間がかかるか計測してみます。
let startDate = Date()// 処理時間計測
print("---ボタンが押された時の処理---")
let tmpstr = JikokuDataList.SearchJikokuData()
resultText1 = String(format: "%@から",tmpstr[0])
resultText2 = String(format: "%@に出るよ",tmpstr[1])
let endDate = Date()
print("Searchにかかった時間は " + String(endDate.timeIntervalSince(startDate)))
これも1ms以下の処理時間なので、問題ないと思います。
実機で動かしてみる
シミュレーターで十分なテストをしたら、実機で動かしてみましょう。昔はアップルのデベロッパープログラムに加入しないといけなかったのですが、最近は加入しなくても出来るようになったようです。
テスト用のiPhoneを用意します。Xcodeをインストールする時に使用したAppleIDと同じIDのものでなければなりません。
まず、Mac OSとXcodeとテストに使うiPhoneのOSを最新のものにアップデートしてください。これをやらないとうまくいきません。
iPhoneをMacとケーブルでつないでXcodeを起動します。
iPhoneのホーム画面に表示されるアプリ名を変更しておきましょう。ナビゲーターウインドウの一番上のプロジェクト名をクリックすると、セッティング画面が表示されます。
GeneralタブのidentityグループのDisplay Nameを変更します。初期値としてプロジェクト名が入っているので、アプリ名に変えます。
次にアカウントを設定します。
セッティング画面のSigning & Capabilitiesを選択します。TeamのドロップダウンリストからAdd an Account...を選択します。AppleIDの入力を求められるので、設定してください。
実機を使ったデバッグの設定をします。iPhoneとMacが接続されていれば、シミュレーターの一覧の上にiPhoneの名前が表示されていると思いますので、それを選択します。
*ここから先はリスクがゼロではないので、ご自分の責任で行ってください。
プレイボタンをクリックしてデバッグを開始します。うまくいけば、iPhoneにアプリが転送され、アプリが起動すると思います。Xcodeのデバッグ画面にデバッグ情報がシミュレーターの時と同じように表示されるはずです。
何らかの理由で転送がうまくいかず、Xcodeがハングアップしてしまう場合があります。その場合は、Xcodeを強制終了してください。optionキーとcommandキーとescキーを同時に押すと、ウインドウが出てくるので、Xcodeを選んで強制終了します。
その後XCodeを起動した時、ビルドが正常に出来なくなることがあります。その場合はshiftキーとcommandキーとKキーを同時に押すとプロジェクトのクリーンアップ(ビルド時に出来る中間ファイルなどをクリアする)が出来るので試してみてください。
デバッグセッションを終えると、転送したアプリはiPhoneにそのまま残り、他のアプリと同じように使えます。
バグ発見
最終バスが出た後に「次の三鷹行きは?」ボタンをタップするとどうなるでしょうか? このSearchメソッドのアルゴリズムだと、配列をサーチして、現在時刻より新しい時刻のデータが見つからないと、結果文字列に何も入れないので、初期値のデータ(乗り場xx、xx:xx)が表示されてしまいます。これはちょっとアレなので、次のバスがない、つまり今日の最終バスはもう出てしまったことがわかるようなメッセージを表示するようにします。
一番簡単な処置は、Searchメソッドが返す文字列の初期値を、最終バスが出てしまった、というメッセージに変えてしまうことですが、それだとあまりにもやっつけ仕事っぽいので、Searchメソッドの初期値を""(空文字)にして、Viewの側でSearchメソッドの戻り値を判定して、空文字の場合は、最終バスが出てしまったメッセージを表示するようにします。
SearchJikokuDataメソッドの結果文字列を、以下のようにします。
// 結果文字列
var ansArray: [String] = ["", ""]
ContentViewで結果文字列を表示する部分を以下のようにします。
let tmpstr = JikokuDataList.SearchJikokuData()
if tmpstr[0] == "" {
resultText1 = "今日の最終バスは"
resultText2 = "もう出ちゃったよ"
}
else {
resultText1 = String(format: "%@から",tmpstr[0])
resultText2 = String(format: "%@に出るよ",tmpstr[1])
}
このコードをテストするには、Searchメソッドで現在時刻を得る部分を以下のようにして、現在時刻を偽ります。ワーニングがわらわらと湧いて出てきますが、気にしなくていいです。
//let nowt = hour * 100 + minute
let nowt = 2250
動かしてみて「最終バスはでちゃったよ」メッセージが出力されるのを確認します。ついでに、nowtを0にして、始発バスが表示されるのを確認してください。テストが終わったら、コードを元に戻しておきます。
潜在的バグ?
調布-三鷹間の時刻表は、22時台が最終なので問題ないですが、24時や25時が最終になっている時刻表を対象にする場合はどうしたらよいでしょう? いまのままのプログラムだと、0時にサーチすると、始発データを先に見つけてしまいます。
これに対応するには、0時以降は最終便の時刻まで現在時刻に24を足してからサーチする、などの工夫が必要になります。
アイコンを作る
Xcodeデフォルトのアイコンはちょっと味気ないので、アイコンを作ってあげましょう。
まず、お絵かきソフトで1024x1024の画像を作ります。私はGIMPを使って背景レイヤーを緑一色にして、その上にネットで無料配布されていたバス停の絵を加え、文字レイヤーに「調布」と「三鷹」の文字を加えた絵を作りました。これをPNGファイルに落としておきます。Appleのガイドラインによると、四隅を丸くしないこと(丸くするのはOSがやってくれます)、あと透過にしてはいけない、とあります。
*余談ですけど、Jobsの伝記に、最初のMacの開発時、アイコンの四隅の丸さにこだわるJobsが、ソフトウェア担当のエンジニアと喧嘩腰でやりあうという、笑えるシーンがあったのを思い出しました。四隅が丸くないアイコンなんて、今では到底考えられないんですけどね。
XcodeのナビゲータウインドウでAssets.xcassetsをクリックし、AppIconを選択してみてください。各デバイス用の異なるサイズのアイコンが必要なことがわかります。GIMPでやっても出来そうですが、すごく大変そうです。
これをやってくれるサービスがネット上にあります。
https://appicon.co
にアクセスして、1024x1024で作ったアイコンをドロップし、Generateボタンをクリックします。
各サイズの画像ファイルをまとめたものががZipファイルとしてダウンロードされるので、必要なサイズのファイルをXcodeにドロップしてやります。
iPhoneをMacにつないでデバッグセッションを開始しましょう。アプリがアイコン付きでアップデートされるはずです。
さらなるアイディア
このアプリに追加するべき機能があるか考えてみます。
(1)逆方向(三鷹⇨調布)のバスも調べたい
先日このアプリを使って、調布からバスに乗って三鷹に行きました。それで、帰る時になって気づいたんです。なんで、逆方向のバスを調べる機能がないの?
スワイプすると画面が切り替わって、次の調布行きを調べられたらいいですね。これは時間ができたらやってみようと思います。
(2)時刻表データをスクレイピングで持ってくる?
今はCSVファイルから読み込んでいる時刻表データですが、スクレイピングでネットから持って来れたらいいですね。時々ですけど時刻表が変更されることもありますし。時間があったらやってみたいです。スクレイピングで持ってきた後、データの永続化をどうするか、について考えるのも面白そうです。
時刻表データをネットのどこかから持ってくるわけですが、webページの見た目に著作権はあっても、時刻表のデータそのものには著作権はないようです。ただし、他人様のwebページからデータをいただくわけですから、サーバーに負荷をかけたりしないよう、気をつけないといけませんね。
(3)バスの接近情報
「乗り場xxからxx:xxに出るよ」と表示した後「少し遅れていま〇〇あたりを通過中」とか、バスの接近情報を表示できたらいいですよね。
京王アプリや小田急アプリ、JR東日本のアプリでは、電車やバスが今どこらへんにいるかを表示する機能があります。位置情報はバス会社や鉄道会社所有のデータで、外部には公開していませんが、ホームページなどで表示している接近情報をスクレイピングする方法を試しておられる方もいるようです。私はそこまでしてやるのもどうかな、と思いますのでパスしたいと思います。
(4)国際化?
今はソースにメッセージを埋め込んでいますが、Appストアにアプリを登録するには(今のところそのつもりはありませんが)国際化のことを考えなくてはいけないそうです。OSの設定の言語設定によって、メニューやメッセージの文字列が切り替えられるようにするんですね。日本で暮らす日本語が母国語でない人もたくさんいらっしゃるでしょうから、日本以外の言語環境であれば、英語の表示にするようにしておけば、喜ばれるのではないでしょうか。
「次の三鷹行きは?」で通じるのは日本語の素晴らしいところで、英語だと"Which terminal will the next bus to Mitaka leave from? I really don’t care about the route."(次の三鷹行きはどの乗り場から出るの? 経路はどうでもいいんだけどね)とかになるのでしょうか。 まあ、実際に言いたいことを正確に表現するとそういうことなので、日本語だと説明が短くて済むというのは、実はいくつかの前提条件を省略して、それは分かっているはずだから、と決めつけているだけなのかもしれませんね。これが日本でよく起きるコミュニケーションミスの原因ではないかと思いますが、だからといって、それを避けるために、厳密な説明をつけて話すと、日本語っぽくないっていうか「あいつ理屈っぽいな」なんてことになってしまうんですよね。「次の三鷹行きは?」ではなく「次の三鷹行きはどの乗り場から出ますか? いくつかの経路の路線があると思いますが、私は経路はどうでもいいんです」では長すぎますよね?
話がそれてしまいました。たぶんこの問題って、わかりやすいシンボルを使えば解決出来そうな気がします。このあたりもそのうち取り組んでみたいと思います。