2015年4月21日火曜日

関数名やクラス名の変更と移行の手順

独自ライブラリを作り進めていくと、関数名やメソッド名の命名ルールを思い付くことがあります。でもライブラリとして使っているので、名前を簡単には変えられません。今までに作ったアプリが、それらライブラリを使っているからです。

もし古いライブラリの関数名などを“絶対に変えたい”なら、上手に移行する手順はあります。言われてみれば当たり前の方法ですが、意外に気付かない人もいるようなので、ちょっと紹介します。

 

実は私も最近、新しい命名ルールを採用しました。数回前の投稿で書いたのですが、独自ライブラリの中で、グローバル関数として作ったときの名前は、「z」を付けて始めるルールに決めました。以前なら「createTxtFldF」と付けるところを、今は「zCreateTxtFldF」と付けています。これで新しいアプリは問題ないのですが、古いアプリは「createTxtFldF」が使われています。それも多数の箇所で。そこで次のように、移行作業を進めています。

まず、ライブラリ本体には、両方の名前で関数を作っておきます。どちらを使われても構わないように。具体的には、次のようなSwiftコードになります。

// テキストフィールドを作成
func zCreateTxtFldF(rText:String, rTextSize:CGFloat, rAlign:NSTextAlignment, 
    rX:CGFloat, rY:CGFloat, rWidth:CGFloat, rHeight:CGFloat) -> UITextField {
    let iTextField: UITextField = UITextField(frame:CGRectMake(rX, rY, rWidth, rHeight))
    iTextField.text = rText
    iTextField.font = UIFont(name:FONT_NAME_DEFAULT, size:rTextSize)
    iTextField.textAlignment = rAlign
    iTextField.borderStyle = UITextBorderStyle.RoundedRect
    return iTextField
}
// テキストフィールドを作成(旧タイプ)
func createTxtFldF(rText:String, rTextSize:CGFloat, rAlign:NSTextAlignment, 
    rX:CGFloat, rY:CGFloat, rWidth:CGFloat, rHeight:CGFloat) -> UITextField {
    return zCreateTxtFldF(rText, rTextSize, rAlign, rX, rY, rWidth, rHeight)
}

見てのとおり、同じ関数を2つ作ってはいません。古い関数を新しい名前に変更し、古い名前の関数は、新しい名前の関数を呼び出す形にします。分かりやすく書くと、次のような形で変更してます。

// 関数名の変更前
func 古い関数名(引数) -> 戻り値 { ... }

// 関数名の変更後
func 新しい関数名(引数) -> 戻り値 { ... }
func 古い関数名(引数) -> 戻り値 { return 新しい関数名(引数) }  // 最終的に削除

こうすると、メンテナンスするのは1つだけになり、片方を変更し忘れるミスが発生しません。もちろん、古い名前で呼び出す場合は無駄な処理が発生して少し遅くなりますが、ほとんど影響はないでしょう。もし影響があったら、その箇所を新しい名前に変更すれば済みますので。

どちらが古いのか、他人が見ても判断できるように、コメントで「旧タイプ」と入れておきます。こういった親切は意外に重要なのです。

独自ライブラリの大元は、以上のような形で、当面は両方を残しておきます。ちなみにライブラリの大元は、iOS実験専用アプリの中に作ってあって、テスト用コードも一緒に保存されています。機能拡張する場合も、iOS実験専用アプリの中で行います。アプリで使い始めるか更新する際に、ライブラリをコピーして、アプリ側に持っていきます。

 

ライブラリを使うアプリ側での更新は、どうなるでしょうか。当然ですが、地道な作業で、1つずつ置き換えていくことになります。次のような手順で。

まず最初に、古いアプリで使われているライブラリのソースコード(古い名前だけが含まれる)を、新しいソースコード(新旧両方の名前が含まれる)に入れ替えます。この時点で、念のためにアプリを動かしてみます。全部の機能を試す必要はなく、主な機能だけです。当たり前ですが、正常に動くはずです。

正常動作を確認できたら、いよいよ名前の入れ替えです。アプリ独自のソースコード全部で、古い名前を使っている箇所を見付け、新しい名前に置き換えます。一括置き換えでも大丈夫なはずです。ソースコード全体で古い名前を検索し、ライブラリにしか存在しなければ置き換えは完了です。この状態でも念のために、アプリを動作させて確認します。

いよいよ最後は、ライブラリのソースコードから、古い名前の部分だけを削除します。削除後も正常にビルドでき、正常に動作すれば、移行作業が完了となります。

 

さて、ここで疑問が生じたと思います。アプリ型にコピーしたライブラリのソースコードからは、古い名前が削除されました。でも、ライブラリの大元には、古い名前の関数などが含まれたままです。違う状態のままで構わないのでしょうか。

上手い方法が見つからないので、仕方ないと思っています。他のアプリでもライブラリを使っていて、まだ古い名前が残っている以上、大元のソースコードから古い名前を削除できません。当分の間は、残しておくしか選択肢はないのです。

ただし、ソースコードが違っているため、管理上の工夫は必要です。バージョン管理の代わりとして、アプリ側のソースコードの先頭に、コメントの形でメモを残しています。その内容は、ライブラリをコピーした日付(これがバージョン番号の代用情報となります)、削除した古い名前のグループ名(たとえば「UI部品の生成関数」とか)です。削除した名前を全部羅列するのではなく、分類上のグループ名で代用し、メモの手間を軽減しています。

このようにメモしたライブラリにも、いつか更新の時期がやってきます。改良版のライブラリと入れ替える場合です。その際、せっかくメモした内容が、新しいライブラリのソースコードにも残るように更新します。次のような手順で。

アプリ内のライブラリ入れ替えは、まず古いソースコードのファイル名を少し変更してから、新しいソースコードのファイルを追加します。その後、古いソースコードのメモ部分をコピーし、新しいソースコードの先頭へペーストしてから、日付部分を当日に書き換えます。そのメモ部分を見て、新しいソースコード内の不要な名前の部分を削除するという手順です。少し手間がかかりますが、仕方がないでしょうね。メモ部分のコピーが終わったら、古いソースコードの改名済みファイルを削除して、ライブラリの入れ替えは終了です。

 

以上は、関数名やメソッド名を変更した場合ですが、クラス名を変更する場合も同じ考え方が適用できます。

クラスでも、クラスの記述が1つになるように、古い名前のクラスが、新しい名前のクラスを参照する形で作ります。クラスの場合、参照する方法としてはサブクラスが適しています。分かりやすい形で書くと、次のような形式になります。

// クラス名の変更前
class 古いクラス名 { ... }

// クラス名の変更後
class 新しいクラス名 { ... }
class 古いクラス名: 新しいクラス名 { }  // 最終的に削除

これだと少し分かりにくいですが、古いクラス名の中身は空で、ブロックを示す括弧記号{}で囲まれているだけです。新しい名前のクラスを継承だけして、変更箇所が何もないクラスを作るというわけなのです。

アプリ側の移行方法も、関数の場合と同様です。まずライブラリを入れ替えてから、アプリが正常に動作するから確認します。正常に動いたら、アプリ側のソースコードで、新しい名前のクラス名に置き換えます。ここでもアプリが正常に動くか確認し、正常ならライブラリ内から古いクラス名の部分を削除します。最後にアプリの動作確認をして、正常に動けば移行が終了です。

アプリ側へコピーしたライブラリのソースコードでも、コピーした日付とともに、削除した名前グループの情報を、ソースコードの先頭にメモとして残します。このあたりの考え方は、関数の場合と同じです。

 

今のところ、バージョン番号の代わりとして、コピーした日付を用いています。バージョン番号が好きなら、それを使っても構わないでしょう。ただし、ライブラリの種類が増えると、バージョン番号がそれぞれ違ってしまい、なにかと気分が良くありません。そんな理由もあって、コピーした日付に落ちついたというわけです。もし上手なバージョン番号の命名ルールを思い付いたら、バージョン番号に切り替えるかもしれません。

独自ライブラリとして作ったソースコードの大元は、前述のようにiOS実験専用アプリの中に入れてあります。アプリごとバックアップしていますが、経歴を残したバージョン管理はしていません。何か思い付いたら、機能を拡張したり、内部を改良したりしています。そのため、時間の経過とともに、少しずつ良くなっています。iOS実験専用アプリ内に残っているのは、常に最新状態だけです。好きなときに細かく改良するので、バージョン番号を付けづらいという面もありますね。

逆にアプリのほうは、リリースした全バージョンを、プロジェクトのフォルダごと残してあります。その中に、ライブラリのソースコードも含まれるため、こちらで経歴を残しているのと同じ効果を持ちます。アプリのほうは、アプリ全体にバージョン番号を付けていて、それが管理番号の役割も持っています。リリースするときには、バージョン番号が必要ですし。

 

今回は、関数銘やクラス名を変更する際の話でした。意外に大変なので、めったに変更しないことだけは表明しておきます。でも、Swiftに十分慣れた時点から作り始めたのではなく、だんだんと慣れながら作り進めたので、最初に作ったソースコードの各部名称は、変更したくなるものなのです。自分なりの命名ルールが固まってきたのは、最近ですから。

一番良いのは、過去に作ったものに関して、あまり気にしないことでしょうか。それができれば苦労しないのですけど。

 

(使用開発ツール:Xcode 6.3, SDK iOS 8.3)

2015年4月16日木曜日

UI部品の繰り返し配置Viewをライブラリ化

業務アプリの特徴として、画面いっぱいにUI部品を配置する場合が多いことが挙げられます。とくに入力画面や更新画面で多く使います。ラベルやボタンも含めると、UI部品の数が100を超えるのは、いたって普通のことです。

UI部品の数は多いのですが、全部が異なる項目というわけではありません。同じ項目が縦に並ぶというケースも多いです。プログラム内部では配列として変数を用意し、それぞれにUI部品を入れた後、画面上で縦に配置するという形になります。異なるUI部品をそれぞれ別な配列に入れ、それらを縦に並べて画面上に配置し、複数行のUI部品の並びを作るという使い方です。

先日、そんな使い方に役立つレイアウト機能をライブラリ化してみました。小さな機能ですが、業務アプリでは意外と便利に使えそうです。今回は、それを紹介しましょう。

 

まず最初は、全体設計です。どのような形で作るべきか、少し悩みました。1つ目の候補としては、UIViewの形でクラスを作り、そのViewに配置するメソッドを用意する作り方もあります。もう1つ候補としては、配置する座標の計算機能だけを用意して、配列内のUI部品をまとめて計算するという作り方も可能です。

どちらが良いのか、両方を使ってみないと判断できそうもありません。仕方がないので、直感で選びました。最終的には、ブループ分けしたUIViewに貼り付けます。そのため、UIViewの形が使いやすいかもしれないと予想し、UiViewのサブクラスとして作ることにしました。

配置するUI部品ですが、ラベルやボタンなどの部品だけでなく、他のUIViewを配置できると利用範囲が広がります。ラベルやボタンも、UIViewのサブクラスと位置付けられます。というわけで、UIView以下の全部を配置対象とすることに決めました。

貼り付ける単位ですが、ラベルなら1つだけ、ボタンやフィールドなどの配列は複数を一括してとなります。1つだけの部品と、配列に入った複数の部品の両方を対象とします。また、配列の場合は、最初から全部ではなく、途中から必要な個数だけの指定も可能とします。実際、1つの配列に入ったUI部品を、画面上の2列や3列に分割してレイアウトする場合がありますから。

配置する位置は、メソッドの中で開始座標として指定します。配列のレイアウトでは、繰り返しでの間隔も指定する必要があるでしょう。

以上をまとめると、次のような内容に整理できます。

// UI部品の繰り返し配置を実現するクラス
・基本的な形式:UIViewのサブクラスとして作る
・配置対象:UIView以下のクラス
・メソッド
 ・初期化:Viewの大きさと位置を指定する
 ・単品の配置:UI部品と配置座標を指定する
 ・配列の配置:UI部品配列と開始インデックスと配置数、配置座標、配置間隔を指定する

ここまで決めると、あとは作り始めたほうが早いでしょう。こんな感じで作り始め、意外にすんなりと作れました。

 

実際のコーディングでは、引数の検査なども入れながら作ります。出来上がったSwiftコードは、次のようになりました。

// 
class RepeatItemsView: UIView {
    // Viewの最小サイズ(単純ミスで変な値を設定した場合の発見用)
    let W_VIEW_MIN: CGFloat = 100
    let H_VIEW_MIN: CGFloat = 30
    // 1行分の高さの最小値(単純ミスの発見用)
    let HEIGHT_LINE_MIN: CGFloat = 5
    // 配列の開始位置座標(左側と上側の余白指定)
    var beginX: CGFloat!
    var beginY: CGFloat!

    // 初期化:Viewサイズと開始位置座標を設定
    func setupF(rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat, _ rBeginX:CGFloat, _ rBeginY:CGFloat) {
        if (rWidth < W_VIEW_MIN) { zSendErrMsgF("ERROR:RV_STUP:Viewの幅が不足"); return }
        if (rHeight < H_VIEW_MIN) { zSendErrMsgF("ERROR:RV_STUP:Viewの高さが不足"); return }
        self.frame = CGRectMake(rX, rY, rWidth, rHeight)
        self.clipsToBounds = true // 領域外は表示させない
        beginX = rBeginX
        beginY = rBeginY
    }
    // 1つのUI部品を配置する
    func addItemF(
        rObj:UIView,         // 配置するUI部品
        _ rPosX:CGFloat,     // UI部品1の横位置X
        _ rPosY:CGFloat      // UI部品1の縦位置Y
        ) {
        // 要素をViewに追加する
        layoutItemF(rObj, beginX + rPosX, beginY + rPosY)
    }
    // 複数のUI部品(配列)を配置する
    func addItemsF(
        rObj:[UIView],       // 配置するUI部品の配列
        _ rBeginIdx:Int,     // 配列の開始インデックス(配列の途中の要素から配置を開始できるように)
        _ rCnt:Int,          // 配置する配列内の要素数
        _ rPosX:CGFloat,     // UI部品1の横位置X
        _ rPosY:CGFloat,     // UI部品1の縦位置Y
        _ rHeightY:CGFloat   // UI部品を配列する1行分の高さ
        ) {
        // 主要な引数の検査
        if rCnt < 1 { zSendErrMsgF("ERROR:RV_ADI:繰り返し数が小さすぎ"); return }
        if rObj.count < rCnt { zSendErrMsgF("ERROR:RV_ADI:配列の要素数が不足"); return }
        if rBeginIdx < 0 { zSendErrMsgF("ERROR:RV_ADI:開始位置が小さすぎ"); return }
        if rBeginIdx > (rObj.count - rCnt) { zSendErrMsgF("ERROR:RV_ADI:開始位置が大きすぎ"); return }
        if rHeightY < HEIGHT_LINE_MIN { zSendErrMsgF("ERROR:RV_ADI:1行の高さが小さすぎ"); return }
        // 複数要素をViewに追加する
        for i in 0..<rCnt {
            let iFinalPosX: CGFloat = beginX + rPosX
            let iFinalPosY: CGFloat = beginY + rPosY + (rHeightY * CGFloat(i))
            layoutItemF(rObj[rBeginIdx + i], iFinalPosX, iFinalPosY)
        }
    }
    // UI部品をUIViewに追加する
    private func layoutItemF(rObj:UIView, _ rFinalPosX:CGFloat, _ rFinalPosY:CGFloat) {
        rObj.frame.origin = CGPointMake(rFinalPosX, rFinalPosY)
        self.addSubview(rObj)
    }
}

いつものように、UIView自体は拡張してないので、UIViewの初期化処理には触らない形で作っています。メソッドsetupFとして、このクラスの初期化処理を付けました。iOSの描画の座標系では、境界ギリギリまで描くと太めの線などで一部が欠けるため、余白は必須です。余白の計算を考えなくて済むように、初期化の中で左型と上側の余白量も指定します。また、UIViewのサイズ指定を単純ミスで間違えたときのために、サイズの最小値を設けて検査を入れました。さらに、領域外にはUI部品を描かない設定にもしてあります。

今回のメソッド引数は、似たような値が含まれるので、正しく理解できるようにと、引数の説明を一緒に入れました。

1つのUI部品をレイアウトするaddItemFメソッドは、指定された値で単純に描くだけです。UI部品のデータ型をUIViewに設定してあるので、UIView以外のインスタンスを指定すると、コンパイルエラーになります。そのため、メソッド側での型チェックは入れてません。

複数のUI部品をレイアウトするaddItemsFメソッドは、指定する引数が多く必要です。指定された範囲が配列から外れないように、それぞれの引数を値を検査しています。また念のために、1行分の高さも極端に小さな値でないか検査しています。

これら2つのメソッドでは、座標位置を計算して、layoutItemF関数を呼び出します。その関数では、座標の値を設定し、UIViewに貼り付けているだけです。

 

いろいろなエラー検査を入れていますが、座標位置に関する検査は、あえて入れないで作ってあります。検査を入れなかった理由は、座標にマイナス値などを入れてアニメーションで画面へ入ってくるとか、いろいろな技を使えるように配慮してのことです。あまりに検査が厳しすぎると、凝った使い方を思いついたとき、制限となってしまいますので。

ここで配置したUI部品は、呼び出し側の配列に入れてあります。当たり前ですが、その配列からUI部品へアクセスして、座標位置などを変更することも可能です。また、他の表示属性も変更できます。あくまで、最初の配置を決めるためのメソッドです。

 

以上のように単純な作りのクラスですが、使う際の自由度は意外に多くあります。

1つの特徴は、座標を細かく指定できる点です。ラベルとフィールドの位置を美しく整えるには、実際に表示して微調整が必要となります。座標を数値で指定できるので、細かな調整が容易です。

通常の使用方法では、UI部品の行数を揃えて、横に一直線に並ばせるでしょう。しかし、UIViewを横に2分割して、片方の行間を小さく作り、より多くの行数を入れるような場合もあるでしょう。配列ごとに1行の高さを指定できるため、そんな要望にも対応可能です。

 

レイアウトに凝ってくると、貼り付けたUI部品を分類するために、区切り線、枠線、部分的に四角く塗りつぶす背景など、情報整理用の部品を描く機能を付けると面白いかもしれません。

また、別な考え方も可能です。このクラスには付けずに、区切り線や枠線などに特化した、分類用描画付きの上位UIViewのクラスを用意する方法です。そのクラスに、いろいろなUIViewのクラスを貼り付けて、全体のレイアウトを整える作り方のほうが良いかもしれません。

どちらにしろ、もし必要になったら、その時点で追加したいと思います。

 

実際に使った例も、いちおう紹介します。具体的な例は複雑すぎるので、単純なテスト用Swiftコードで。全部を掲載すると長すぎるので、配列に値を設定する部分などを省略しています。

// 以下のテスト用コードを、UIViewControllerを継承したクラスに作る
// テストに使う主要な変数
var iRIView: RepeatItemsView!
var aryBtn21: [UIButton] = [UIButton]()          // ボタンの配列
var aryTxtFld21: [UITextField] = [UITextField]() // テキストフィールドの配列
let iLabelBtn: UILabel = zCreateLblF("ボタン列", 14, ALIGN_LEFT, 0, 0, 60, 24)
let iLabelTxf: UILabel = zCreateLblF("フィールド列", 14, ALIGN_LEFT, 0, 0, 100, 24)

// 配列にUI部品を入れる
aryBtn21.append(zCreateBtnF("Btn1", 15, 10, 10, iW, iH))  // これを数回繰り返す
aryTxtFld21.append(zCreateTxtFldF("Field1", 18, ALIGN_LEFT, 10, 10, iW2, iH2)) // これも数回繰り返す
// ボタンへタップ動作を設定
aryBtn21[0].addTarget(self, action:"btn21Go01F:", forControlEvents: .TouchUpInside) // これも数回繰り返す

// Viewの生成とレイアウト
iRIView = RepeatItemsView()
iRIView.setupF(0, 0, 600, 300, 10, 10)
iRIView.addItemF(iLabelBtn, 10, 0)                                 // ボタン列のラベル
iRIView.addItemsF(aryBtn21, 0, aryBtn21.count, 10, 30, 40)         // ボタン列
iRIView.addItemF(iLabelTxf, 110, 0)                                // フィールド列のラベル
iRIView.addItemsF(aryTxtFld21, 0, aryTxtFld21.count, 110, 30, 40)  // フィールド列
viewBase.addSubview(iRIView)    // 生成したViewの表示

見てのとおり、UI部品の生成関数を使って短くし、さらに細部を省略した準備のコード行数と、レイアウトするコード行数が同じぐらいです。それだけ、最小限の手間で使えるという証拠でしょう。

UI部品の表示位置は、座標の数値指定なので、実際に画面を表示させて確認します。ボタンとボタンの間隔、ボタンとラベルの間隔など、表示が整って見えるような位置を探しながら微調整することになります。

 

それぞれのUI部品に関係する処理内容は、このクラスを呼び出す側で作ります。ボタンをタップした場合の処理とか、フィールドの値を変更した時の処理とか、すべての呼び出し側の責任でUI部品に関連付けます。

このクラスの役割は、繰り返し配置を手助けすることと、配置した結果のUIViewを提供することです。それだけの機能でも、呼び出す側のコードを単純化でき、結果として修正しやすいコードにつながります。

 

今回は、UIViewのサブクラスとして作りましたが、最初に悩んだように、表示位置だけを設定する関数の形でも作れます。簡単に作り終わったので、両方あって使い分けても良いかなと思うようになりました。当面は、今回のクラスで十分に間に合うので、また機会があれば関数バージョンも作ってみようと考えています。

今回のような小さな機能は、探せばまだ見付かりそうです。また何か見つけたら、作って独自ライブラリに追加しようと思います。

 

(使用開発ツール:Xcode 6.3, SDK iOS 8.3)

2015年4月12日日曜日

Xcodeを6.3に更新しました

先日、理不尽な更新によって、Xcodeが6.2になった話を書きました。

その後、以前に作ったアプリの簡単な機能追加を頼まれたので、Xcode 6.2のまま作成して納品しました。その際、シミュレーターでは正常に動き、実機でのみクラッシュするという、修正してない箇所での新しい問題が生じました。原因を見つけてソースコードを修正し、無事に作り終わりました。やはり、互換性問題は生じるものですね。

新しいアプリのほうは、本番でも無事に動いているようです。大きな問題が発生せず、安心しました。

 

こんな感じで過ごしているうちに、Xcodeの6.3が正式に公開されました。Swiftがバージョン1.2となり、細かな機能アップが含まれています。

実験マシンではベータ版をインストールして、iOS実験専用アプリを開いて見ていました。キャスト関係の仕様が変わったようで、多量のコンパイル・エラーが出ていました。その場で直そうかと思ったのですが、正式版が出てからでも遅くないだろうと、とりあえず無視してました。

正式版が登場したので、同じように実験マシンにインストールして、様子を確認です。以前と同じように、多量のコンパイル・エラーが出ていました。これは大きな問題ですね。

まだ使わないから放置するという選択もあるのですが、SwiftやAPIの仕様も変わったことですし、いつかは対応しなければなりません。新しいXcodeを使う必要が生じたとき、互換性エラーの修正から始めるのでは、たまったものではありません。今回はエラーの数が多いので、事前に修正しておいた得策だと思いました。

という考えから、あまり乗り気ではないのですが、開発マシンのXcodeも6.3に更新しました。Swiftの正式版が登場してから、まだ1年も経過していません。今までも少しずつ仕様変更がありましたが、登場から2年間ぐらいは、同じような状況が続くのでしょうね。新しいプログラミング言語なので、更新に付き合うしかないと諦めました。正直、できるだけ早めに、安定して欲しいです。

 

Xcode 6.3への対応は、iOS実験専用アプリから始めています。いろいろなライブラリが含まれていて、最初に試すのには一番良いアプリです。この際ですからSDKを最新の8.3に設定して、エラーを消すように修正しました。

Xcodeには、Swiftの仕様変更に自動で対応させる機能があり、一括してソースコードを更新できます。これを使えば手間を省けるのですが、どんな箇所がどのように変わったのか、知れる機会を失います。余計な手間はかかるものの、1つ1つエラーメッセージを読みながら、手動で修正する道を選びました。

 

キャスト関係の変更が、一番影響ある部分でしょうか。今までは「as」とキャストしていた箇所を、「as!」と書くように求められます。

DictionaryとNSDictionaryのキャストも仕様が変わり、データ型を明記するように求められます。描画機能でも描画属性でNSDictionaryが使われていますが、ここでもデータ型を明記してキャストするように求められました。

split関数でもエラーが出ました。記述方法が少し変わったようです。分割条件式ブロックの前に「isSeparator:」を付けるように求められます。

UIButtonなどで使われているtouchesBeganなどの関数(UIResponderに含まれている機能です)では、touchesのデータ型が、NSSetからSwiftのSetに変更されました。ボタンなどの機能を拡張している箇所で、データ型を修正しなければなりませんでした。

今回たまたま気付いたのですが、iOS関連でも警告を受けました。NSCalendarのidentifierで、NSJapaneseCalendarをNSCalendarIdentifierJapaneseに変更しなさいという警告が出ていました。少し調べてみると、iOS8から非推奨になっているようで、いずれ使えなくなるのでしょうね。

他にもあったと思いますが、思い出せたのは以上です。

 

iOS実験専用アプリは、エラーが消えてビルドに成功しました。オフしている機能もあるので、順番にオンしながら、正常に動くかどうか確かめたいと思います。

他のアプリも、いずれ対応させなければなりません。新しく作った独自ライブラリも増えてきたので、ライブラリを組み込みながら、Swift 1.2への対応も一緒に進めようと考えています。

アプリの数が多い人ほど、大変そうですね。皆さんも、地道に頑張ってください。

 

(使用開発ツール:Xcode 6.3, SDK iOS 8.3)

2015年4月6日月曜日

SwiftとiOSの変化を意識したアプリ開発

今回は、いつもと趣向を変えて、違う話題を取り上げてみます。SwiftやiOS APIの将来について、開発ツール選びや開発方針との関連も含めて、ちょっと考えてみたいと思います。

 

開発ツールやAPIを選ぶことは、開発者にとって大事な選択です。とくに大きなソフトを作る場合、開発ツールやAPIの将来性を適切に見極めないと、時間をかけて開発したものが無駄になったりします。同様に、小さなソフトでも数が増えると、大きなソフトを作ったと同じ状況に陥ります。

見極めが大事だと書きましたが、開発ツール全体の傾向としては、少しずつ不安定な方向に変化していると感じています。昔のように、1つの言語をマスターすれば、安心して長い間使えるという時代ではなくなっていると感じます。上手に対応するためには、複数の言語やAPIを使えなければならない時代なのでしょう。見極めと同時に、どれか1つだけに固執しないのも、これからの時代は大事なのかもしれません。

 

で、肝心のSwiftとiOS APIの話です。これらも将来の変化を意識する必要があるでしょう。まずはSwiftから。

今のところSwiftは、アップルの開発ツールとして大きな地位を築きつつあります。築きつつというより、固めつつあると表現したほうが適切かもしれません。その流れを作っているのは、当然ながらアップルの方針です。

おそらくアップルは、開発用のプログラミング言語を、Objective-CからSwiftに移行したいのでしょう。現状では、Objective-Cを完全に無くすことはできませんから、段階的に移行する過程だと思います。今後の大きな流れとしては、Swiftの仕様を少しずつ拡張しながら成長させて、Objective-Cから完全に置き換える方向に向かっているのでしょう。

 

続いて、iOSのAPIです。現状のAPIは、OSXのAPIをできるだけ流用しつつ、座標などを少し変えて手直ししたものです。細かなクラスなどは、iOSとOSXで共通のものが多くあります。それは当然の選択で、あえて同じに作ることで、ソースコードを流用しやすくしています。

では、今後も今のAPIを使い続けるのでしょうか。有名な噂サイトに登場した噂話によると、どうやら新しいAPIを作っているようなのです。おそらくは、iOSとOSXで完全に共通のAPIとして作っているでしょう。iOSとOSXは、UIも少しずつ近づけていますから、これも当然の流れだと思います。

 

その新しい共通APIは、どのような形になるのでしょうか。当然の流れとして考えられるのは、Swiftとの親和性を強めることです。

現状のSwiftでは、iOS APIとの親和性が十分ではありません。たとえば、Dictionaryなら、NSObject以下のクラスを自由に扱えるのはNSDictionaryであって、SwiftのDictionaryではありません。同様にAnyも、NSObject以下のクラスとの親和性がAnyObject(中身はObjective-Cのid)に劣ります。

これらの欠点を解消し、SwiftのDictionaryやAnyとの親和性の高いAPIになるはずです。それ以外の面も含めて、Swiftとの親和性を第一に考えて設計されたAPIとなるでしょう。

その新しい共通APIは、Objective-Cからも使えるでしょうか。そのときになってみないと分かりませんが、アップルのことですから、使えなくすることも十分に考えられます。Swiftへの移行を強く進めるために。

 

おそらくは、APIの共通化だけが目的ではないでしょう。もっと現代的な設計に作り変えているのではないかと、予想します。

私だけの印象かもしれませんが、NSObjectをトップに持つ一連のクラスは、昔ながらのオブジェクト指向を強く意識した形で作られているように見えます。デリゲートなどの作り方に癖があるというか、全体的に美しくありません。また、使いやすくないことも大きな欠点だと思います。

アップルがどのような形に改良するかは不明ですが、おそらくは癖をあまり出さないで、ソースコードが書きやすい形でまとめ上げると予想してます。

 

つまり全体の流れとしては、プログラミング言語はObjective-CからSwiftに、APIはiOS/OSXの2つのAPIから共通モダンAPIに、という形になると予想します。当分の間は、それぞれの古いもの(Objective-Cと、iOS/OSXの2つのAPI)もサポートされるでしょうが、iOSやOSXのバージョンがどんどん上がるうちに、サポートしなくなる可能性が高いでしょう。

もう1つ、iOSとOSXという2つのOSの存在も、今とは違ってくるでしょう。おそらく、これも共通になり、1つのOSに統合されるはずです。UIが少しずつ近づいているわけですから、共通化も少しずつ容易になっていきます。もちろん現実は逆で、共通化する目的のためにUIを近づけているのでしょう。

アップル独自のチップであるAシリーズも、世代が進むごとに処理能力が向上しています。あと数世代も進めば、現状のパソコン用CPUと同等か、それ以上の処理能力を得られるでしょう。そうなったときが、共通OSへの移行に適した時期ではないでしょうか。

共通OSになったとき、iPhone、iPad、Macというハードウェアの区別に、あまり意味はありません。iPhoneの画面の大きな機種に、Xcode後継ツールをインストールして、無線キーボードと組み合わせながら、iPhone上でアプリ開発も十分に可能でしょう。実際に使うかどうかは別として。

 

以上のような流れですが、大きくは外れないと思います。大事なのは、プログラミング言語はObjective-CからSwiftに、APIはiOS/OSXの2つのAPIから共通モダンAPIに、という流れです。

いろいろな開発者がいますから「流れなんて考えず、また作り直せばいいよ」と思う人もいるでしょう。でも、作り直すのは大変です。誰にも頼めないユーザーもいるでしょう。作り直しだと余計に手間や費用がかかり、そこまでの費用を出せないユーザーもいるでしょう。どう考えても、できる限り最小限の手間で、使い続けられることが一番良いのです。

将来の手間を最小限にするためには、現時点でどんな選択をすれば良いのでしょうか。Xcodeを使ったiPadアプリ開発に限って考えると、次のような点が大事だと思います。

まずは、プログラミング言語の選択です。可能な限りSwiftで書くことは必須です。もしObjective-Cしか作れない機能があったとしても、全体をObjective-Cで作るのはお勧めできません。作れない部分だけ切り分けてObjective-Cで書き、残りの部分をSwiftで書くのがベストです。

APIに関しては、新しい共通APIが登場していませんから、現状のAPIを使うしかありません。でも、将来への対処方法が、まったく無いわけではありません。どのアプリでも共通で使う機能は、できるだけライブラリとして作り、すべてのアプリで共通して使えるように作っておきます。将来、新しいAPIが登場したとき、個々のライブラリを新APIで作り直して、入れ替えることを可能にしておきます。

共通ライブラリを使ったアプリに仕上げることで、入れ替え用の互換ライブラリを1つ作るだけで、どのアプリにもそのまま利用できます。ライブラリとして作るわけですから、テストも試験専用アプリで徹底的に行えます。アプリ側の細かくテストする必要はありません。徹底的にバグを消してから、アプリ側を入れ替える手順が可能です。

いくつものライブラリとして作っているので、1つずつ順番に新APIで作り直すことができます。それぞれのライブラリは関連する機能が集まっているので、新APIの仕様を調べるのも効率的でしょう。同様に、細かなテストも作りやすいはずです。

ライブラリ化の大きなメリットは、ライブラリ化してないアプリを新APIに移行する作用を想像すると、よく分かります。ライブラリ化してないのですから、すべてのソースコードが、それぞれのアプリで異なります。新APIに書き換えるときは、アプリごとに細かく見ながらソースコードを書き直していきます。テストも個々のアプリごとに、別々に行う必要があり、非常に大変な手間となるでしょう。

当然ですが、共通ライブラリ化の一番のメリットは、将来への備えではありません。共通化することで、新しいアプリを作るときの手間を大きく減らせることが、最大のメリットです。さらには、ライブラリにバグが見つかったとき、バグを修正すれば、すべてのアプリで入れ替えが容易です。アプリごとに個別に直す必要はありません。こうしたメリットに加えて、将来への備えにもなるのですから、やはり非常に大きなメリットと言えるでしょう。

 

長くなりましたが、ここまでの話をまとめると次のようになります。

現状のiPadアプリ開発で重視したい点
・できるだけSwiftで作る(Objective-Cでなければ作れない箇所は、その部分だけObjective-Cで作る)
・どのアプリでも共通に使う機能は、関連する機能を集めてライブラリ化して、取り替え可能に作る

以上は、あくまで私個人の考察結果です。信じるのも自由、一部だけ信じるのも自由、まったく信じないのも自由です。どちらにしても、将来起こる変化を少しは気にして、アプリ開発の方法を考えてみてはいかがでしょう。