2015年7月7日火曜日

ライブラリ化の当面のゴール

このブログで紹介しているように、Swiftで書いた独自ライブラリを少しずつ作り進めています。まだまだ途中の段階ですが、どんなゴールを描いているのか、簡単に紹介したいと思います。

最初に言っておきますが、これから書く話は、ゲームなどのアプリを作るのが前提ではありません。あくまで業務アプリなどの、遊び要素の少ないアプリの開発に関しての話です。ゲームのアプリなら、大規模な開発ツールが既に作られていて、それを使うのが賢い選択でしょうから。

 

新しいOSや開発ツールを使い始めたとき、独自のライブラリ化を進めるのは、開発経験が長い人なら一般的でしょう。ただ、どのようにライブラリ化するのか、どんなゴールを目指して進めるのかは、人によって異なると思います。

私の場合は、開発やメンテの手間を減らすことです。それにより、開発期間が短くなりますし、完成後のメンテの負荷も小さくなります。とにかく幅広く使えるライブラリを増やして、アプリ開発の手間を大きく減らすことが目的です。

ライブラリとして作ったコードは、単独で十分なテストをされているのに加えて、複数のアプリで使われているため、品質は高く保たれがちです。また、OSやツールのバージョンアップで変更が生じたときも、ライブラリ単体で更新してテストを済ませれば、あとはアプリ側のライブラリを入れ替えるだけです。複数アプリを合計した際の作業量は、かなり減るでしょう。

あと、ライブラリの形式として、ソースコードのままで使う方法を重視しています。ソースコードのままアプリに追加する形なので、ライブラリにバグがあっても簡単に見付かりますし、ライブラリを拡張したくなったときも、可能性が簡単に判断できます。ソースコードのまま追加するものの、基本的にはライブラリに手を加えません。ただし、どうしても手を加えなければならないケースでは、ソースの先頭に変更内容を明記する形で、認めてはいます(実際には、めったにやりません)。当然ながら、ライブラリを改良して対応するのが、第一の選択肢です。

以上は、あくまで、目先の目的としての話です(もっと大きな目的は、最後のほうに出てきます)。

 

いろいろなアプリを作っていると、似たようなコードを数多く書いている自分に気付きます。たいていは1から書かず、前に作ったソースコードをコピーしてから修正するでしょう。コピーによって手間は減っていますが、似たようなコードを書いている現状には変わりません。

そんな風に考えていると、似たようなコードを書かなくて済む方法はないか、深く考えるようになります。部分的にコピーして作るのではなく、ソースコード全体をファイル単位でアプリに持って行き、そのままで使える状態に作れないかと。これがライブラリ化の始まりです。おそらく、独自のライブラリを作っている人は、似たような経験や考えがあるはずです。

実際にライブラリ化を進めると、かなりの効果があると実感できます。似たようなコードを書くことが減り、ライブラリで済ませられる範囲が少しずつ増えていきます。もし既存のライブラリで機能が不足する場合も、ライブラリのほうを拡張して、新しいアプリに対応させます。その結果、ライブラリが適用できる範囲が少しずつ増えていきます。

Swiftを使い始めてから、それほど時間が経過していません。1.0の正式版が登場してからですから、まだ1年未満です。それでも、ライブラリを少しずつ作り続けたので、だんだん充実してきました。モデルクラスのライブラリも2つ作り、ライブラリの対象範囲も広がっています。今後の充実が、ますます楽しみです。

 

続いて、別な視点でライブラリを考えてみます。ライブラリ化する機能の種類で考えると、ライブラリ化のゴールが見えやすくなります。

真っ先に作ったのは、どのアプリでも必ず使う機能でした。UI部品の生成、日付と時間、ファイルの閲覧、電子メールの送信、描画機能などです。それらが作り終わると、環境設定など、どのアプリでも作りそうな機能をライブラリ化しました。こうした種類のライブラリ化は今も続けていて、ライブラリによるサポート範囲をさらに広げようとしています。

アプリ独自の要素として作らなければならない機能は、扱うデータを操作するモデルクラス、それを画面に表示して操作するビューとコントローラのクラスでしょう。作る量が大きい順に並べると、ビュー、モデル、コントローラとなります。

このうち、モデルのクラスは、すでにライブラリ化を進めています。まだ種類は少ないですが、少しずつ種類を増やして、ライブラリで済ませる率を向上させる予定です。

ビューは、モデルのように、個々のモデル全体をライブラリで作る形が難しい分野です。ビューの一部をライブラリとして用意し、それらを組み合わせて1つのビューを実現します。つまり、部品として作るのが適している分野です。ただし、機能の種類が多くあるため、ライブラリでカバーできる比率は、なかなか上がらないでしょう。

当面の方向性としては、次のように考えています。ビューを実現するための要素を、幾つかのタイプに分類して、タイプごとにライブラリを増やすつもりです。今までは、部品となる小さなビューだけでした。しかし、モーダルビューのように、動きの実現がメインとなるライブラリも、今後は増えていくでしょう。また、ビュー上にUI部品を繰り返して並べるような機能も、ライブラリとして作りました。これなどは、配置機能に特化したライブラリです。さらには、いろいろなUI部品を生成する関数群、同じUI部品を一気に生成する関数群など、ビューの構成部品の生成もライブラリ化しています。

あと、紹介していませんが、環境設定の専用ビューもライブラリを試作しました。以前に紹介した、環境設定の機能をライブラリ化したものと連動して、環境設定の値を変更するためのビューを生成します。凝った機能には対応できないレベルですが、一般的な設定の範囲であれば、もう使える状態です。以前紹介した環境設定のライブラリを使うと、環境設定の値を定義するだけで、環境設定の機能が作れます。ビューも同様で、環境設定画面の定義を用意するだけで、環境設定を変更するビューが作れてしまいます。これら2つは連動するので、環境設定機能とビュー機能のやり取りは、コーディングする必要がありません。このように、特定の機能を実現するビューなら、ライブラリとして、まだまだ作れる範囲があると思います。

 

アプリ全体として見た場合、ライブラリが適用できなくて、そのアプリ専用にコーディングが必要となる領域は、ビュー関係に一番多く含まれます。つまり、ライブラリ化の一番の肝は、ビューに関する機能のライブラリ化をどれだけ実現できるかです。モデルに関しては、5種類ほど作れれば、半分以上のモデルをライブラリで作れるでしょう。ビューでは、そのような考え方が通用しません。どんな切り口でライブラリ化したら良いのか、どうすれば新しい工夫ができるかが、ビューに関するライブラリ化の度合いを決めます。

今後は、さらにレベルアップできないか検討中です。もう少し違った切り分け方で、ビューの機能を実現できないかと、かなり考えています。まだ結論は出ていないものの、もしかすれば何とかなりそうなヒントだけは思い浮かびました。これを発展させて、新しい種類のライブラリが作れたら、ビューに含まれる機能のライブラリ比率が、かなり向上させられそうです。

 

アプリとして作る機能のうち、モデルとビューでのライブラリ比率が向上できると、アプリ全体でのライブラリ化率がかなり向上できます。

ライブラリなしで開発したときのコード量を10割としたとき、ライブラリの使用により何割減らせるかが、ライブラリの効果を評価する際の数値になると考えています。ライブラリなしで開発したときのコード量というのも、やや大雑把ですね。あえて説明すると、すべての機能をベタで書いたとき(雑誌やウェブでのサンプルのようなコード)のコード量という意味です。

Swiftのライブラリ化を始めた頃は、どの程度まで作れるのか予想が付きませんでした。漠然とですが、遠い将来は5割が目標だけど、当面は3割ぐらいかなと考えていました。

ところが、ライブラリが増えるごとに、それもライブラリの対象範囲が広がるとともに、5割は簡単に実現できると思うようになりました。実際、今まで作ったライブラリの機能だけに限定したアプリを作るなら、5割ぐらいはライブラリで済ませられ、残りの5割をコーディングする形になりそうです。

もし新しい機能を使うアプリを開発する場合は、その新しい機能の部分も、ライブラリとして作ります。そうすると、ライブラリが対応できる機能が増え、似たようなアプリを再び開発する場合は、やはり5割ぐらいがライブラリで対応できる状態になります。

今では、将来的に7割ぐらいも可能じゃないのかと、思い始めています。前述のビュー機能のライブラリ化が、どこまで実現できるかにかかっています。ただし、ライブラリがいくら充実しても、7割強ぐらいが限界ではないかと思います。ライブラリとして作りにくい機能が、ある程度は残りますから。

 

以上の話は、コード量に関してだけです。アプリ開発では、コードを作る以外の作業が多く含まれます。それが減らない限り、開発の負荷は大幅に減りません。その部分に関しては、かなり難しいでしょう。

それとは別に、開発スタイルが大きく変わる可能性はあります。新規に作らなければならないコードの開発量が減ると、とりあえず試しては変更し、異なる仕様を何度も試せます。ライブラリで実現した機能は、使う際の定義データや使い方を変更するだけで、ある程度まで機能を変えられます。もし機能が不足する場合は、ライブラリを拡張して、新しい機能を追加するでしょう。どちらにしても、いろいろと変えながら試せる可能性は、今よりもずっと高まります。

このような開発スタイルが、ライブラリ化を進めている当面のゴールです。それを実現するために、ライブラリで作るコードの割合を7割ぐらいにするのが、別な視点での当面のゴールとなります。

ライブラリを使って作ったアプリは、俗に言うプロトタイプではありません。本番でも使える、実際に動くアプリです。本番用のアプリを、プロトタイプ的に作ろうというのが、ライブラリ化の本当の目的と言えるかもしれません。

 

今回は趣向を変えて、ライプラリが充実したときの状態を、当面のゴールという形で書いてみました。具体的なコードばかり見ていると、大きな目標を見失ったりします。せっかくですから、大きな目標を掲げて、それに向かって努力したほうが、良い結果が得られるでしょう。これを読んだのを良い機会にして、自分なりの当面のゴールを、考えてみてはいかがでしょうか。

2015年7月2日木曜日

共通モデルのライブラリ化を試作(機能追加編2)

共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話を紹介しました。前回の投稿で書いたとおり、内部配列を変更しました。その後に、新しい機能を追加したので、また紹介します。

 

今回追加したのは、ソート機能。説明する必要のないでしょうが、指定された項目値の大きさで、レコードを並び替える機能です。

最初に決めなければならないのは、ソート機能の組み込み方でしょう。ソートしたデータをどのように利用するのか、いろいろと考えました。ソート結果をモデルクラス内で持ち、順番に取り出す方法も考えられます。しかし、そうするとソート以外の機能を用意しなければならず、作る量が増えてしまいます。また、用意した機能で不足する状況も考えられます。

それを避けるために、ソート後のレコード番号を配列の形で渡すことにしました。レコード番号さえあれば、あとは自由に使えます。レコード番号を指定して、レコード内の項目値が得られますし。

ソートには昇順と降順があります。昇順の配列は、逆から読むと降順として使えます。そのため、昇順のソートだけを作ることにしました。

 

実行に必要なのは、どの項目でソートするかの指定だけですから、引数は項目番号だけで済みます。また、戻り値はレコード番号の配列です。データ型が違っても、まったく同じです。というわけで、メソッドは1つだけになりました。

ただし、Bool型だけは値が2種類しかないので、ソートする意味がありません。無駄な処理をさせないために、エラーメッセージを出して終了することにしました。

では、具体的なコードを見ましょう。次のようなSwiftコードを追加しました。

// ======================================== ソート:レコード番号の配列を返す
func sortF(rFldIdx:Int) -> [Int] {
    if cAryType[rFldIdx] == .ZmBool { zSendErrMsgF("ERROR:ZMR_SORT:項目定義のデータ型がBoolでは使用不可"); return [Int]() }

    // Step1:指定された項目値とレコード番号のペア配列を要素とする、レコード全体の2次元配列を作る
    var iAryAnySort: [[AnyObject]] = []  // ソートで使う2次元配列:データは[[項目値, RecIdx], ...]の形になる
    for i in 0..<cRecCount {   // i=RecIdx
        var iAnyPair: [AnyObject] = []        // 項目値とRecIdxのペアを入れる配列
        iAnyPair.append(cAryDataAny[rFldIdx][i])  // 指定された項目の値
        iAnyPair.append(i)                        // RecIdx
        iAryAnySort.append(iAnyPair)          // ペア配列をソート用配列に追加
    }

    // Step2:指定された項目値で、2次元配列をソートする:ペア配列の最初の要素を取り出して、大小を比較する
    // ペア配列(a0とa1)の最初の要素(a0[0]とa1[0])を取り出して、大小を比較する
    switch cAryType[rFldIdx] {
    case .ZmInt:
        iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! Int) < (a1[0] as! Int)) }
    case .ZmFloat:
        iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! Float) < (a1[0] as! Float)) }
    case .ZmStr:
        // 英字の大文字小文字に加え、全角英字の大文字小文字の違いを、すべて無視してくれるソート
        iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return
            (a0[0] as! String).localizedCaseInsensitiveCompare(a1[0] as! String) == NSComparisonResult.OrderedAscending }
    default:
        zSendErrMsgF("ERROR:ZMR_SORT:ここは通らないはず:実行されたらバグあり")
        return [Int]()
    }

    // Step3:2次元配列から、レコード番号だけの配列を作る
    var iAryRecIdx: [Int] = []
    for i in 0..<cRecCount {   // i=RecIdx
        let iRexIdx: Int = iAryAnySort[i][1] as! Int  // ソートされたRecIdxを取り出す
        iAryRecIdx.append(iRexIdx)
    }
    return iAryRecIdx
}

いつもより長めのコードで、全体が3つの処理に分かれているので、Step1〜3の表記をコメントにつけています。これで、処理の区切りが明確に伝わります。

3つの処理の前に、指定された項目がBool型かを検査します。該当する場合は、エラーメッセージを出して空の配列を返します。空の配列を返すのは、戻り値をOptional型にしたくないからです。

 

3つのステップを順番に見ていきましょう。

まずは、ステップ1です。1つの配列をソートするだけでは、並び替えたレコード番号が得られません。また、項目値を入れたAnyObject配列には、レコード番号が入っていません。

レコード番号と一緒にソートできる配列を、新しく用意する必要があります。ソートする項目値とレコード番号をペアにして配列に入れ、それをレコード数の分だけ並べます。その集まりを、ペアの中の項目値でソートすると、レコード番号も一緒に並び替えられ、ソート後のレコード番号が得られるというわけです。

つまり、2次元の配列を用意する必要があります。このような配列を作るために、レコード数だけforループさせ、ペアとなる配列iAnyPairを毎回作って、ソート用の配列iAryAnySortへ次々と入れていきます。

続いて、ステップ2です。配列iAryAnySorが出来上がったら、その配列自体をソートします。sortメソッドを使い、値の大きさを判定するための関数(クロージャ)を渡すだけです。ここでは、データ型を指定する必要があるので、項目のデータ型ごとに処理を分けています。

このクロージャの作り方は、少し注意しなければなりません。ソートする配列は、2次元のAnyObject配列です。ですからsortメソッドが取り出して比較するのは、1次元のAnyObject配列になります。そのままだと比較できないので、1次元配列の最初の要素(項目値)を取り出し、Int型にキャストして比べる必要があります。そのため、(a0[0] as! Int)や(a1[0] as! Int)という形になっているわけです。また、引数のデータ型も、ちゃんと[AnyObject]を指定しています。

このsortメソッドが実行されると、2次元配列iAryAnySort内で、ペア配列が項目値の順に並べ替えられます。あとは、順番に取り出すだけです。

ステップ3の取り出しの処理では、戻り値となるレコード番号の配列を作りながら、レコード数の回数だけforループを実行します。ループが終了したら、レコード番号の配列を返して終了です。

 

今回のメソッドの中で一番理解しづらいのが、ステップ2で使っているsortメソッドのクロージャでしょう。普通の配列をソートするのではなく、2次元配列をソートしていますから。

処理内容が少しでも理解しやすいようにと、クロージャはかなり親切に書いています。何も省略しない書き方なので、これが精一杯でしょう。あとはコメントで補足するしかありません。コメントも、短いながら丁寧に書きました。

// 省略しない書き方
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! Int) < (a1[0] as! Int)) }

// 省略した書き方
iAryAnySort.sort { ($0[0] as! Int) < ($1[0] as! Int) }

逆に省略した書き方も可能です。でも今回のような使い方では、クロージャ内の引数のデータ型が書かれていないと、前の処理を見ながらデータ型を調べる必要があります。結果として、何をしているのか理解しづらくなります。

少し変わった形でソートする場合は、sortメソッドに渡すクロージャを、できるだけ理解しやすく(省略なしで)書いたほうが良いと思います。もちろん、コメントでの補足も忘れずに。

 

ステップ2のソートの処理では、String型だけが違う書き方になっています。

他のデータ型のような不等号だけでは、期待したようにソートされず、使いものになりません。英字には大文字と小文字があり、それらを区別してソートしてしまうからです。さらに、日本語にも全角の英字が含まれ、それにも大文字と小文字があります。これらも区別してソートしてしまいます。数字の全角と半角も同様に、区別してソートしてしまいます。

望まれているソートの形は、もっと違うものです。半角英字の大文字と小文字、全角英字の大文字と小文字、それら4つの種類を区別しないで、同じ英字として処理してくれるソートなのです。

このような目的のために、ローカライズされた文字比較の機能が用意されていて、String型で使えます。具体的にはlocalizedCaseInsensitiveCompareメソッドで、比較したい文字列を渡し、判定結果がNSComparisonResult型で返ってきます。

2種類の比較方法を、あえて並べて書くと、次のようになります。

// 英字の大文字小文字に加え、全角英字の大文字小文字の違いを、すべて区別するソート
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return ((a0[0] as! String) < (a1[0] as! String)) }

// 英字の大文字小文字に加え、全角英字の大文字小文字の違いを、すべて無視してくれるソート
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return
    (a0[0] as! String).localizedCaseInsensitiveCompare(a1[0] as! String) == NSComparisonResult.OrderedAscending }

今回のソート機能では、下側のほうを使いました。これで、英字の大文字や小文字が含まれていても、全角の英字や数字が含まれていても、アルファベット順や数字順で正しくソートしてくれます。

 

String用ソートでも、できるだけ理解しやすいようにと、省略しない形で記述しています。逆に、少し短くした記述方法も可能です。今回の場合は、あまり短くなりませんが。

// 省略しない書き方
iAryAnySort.sort { (a0:[AnyObject], a1:[AnyObject]) -> Bool in return
    (a0[0] as! String).localizedCaseInsensitiveCompare(a1[0] as! String) == NSComparisonResult.OrderedAscending }

// 省略した書き方
iAryAnySort.sort { ($0[0] as! String).localizedCaseInsensitiveCompare($1[0] as! String) == NSComparisonResult.OrderedAscending }

String用の場合も、少し変わった形のソートですから、省略した記述は避けたほうが良いでしょう。

 

一応、使用例も紹介します。非常に簡単です。いつものように、iOS実験専用アプリから抜き出しました。

// ソート用メソッドを使ってみる
let iAryKey1: [Int] = modelRecCard25.sortF(fNumber_i)  // Int型の項目でソート
let iAryKey2: [Int] = modelRecCard25.sortF(fRate_f)    // Float型の項目でソート
let iAryKey3: [Int] = modelRecCard25.sortF(fName_s)    // String型の項目でソート

メソッドの引数として、項目番号を指定します。項目番号を間違えないようにと、項目番号に名前をつけた変数(定数)を使うのが基本です。

どの項目のソートでも、たった1行書くだけで、ソート結果が得られます。昇順に並んだレコード番号の配列です。あとは、そのレコード番号を使ってモデルクラスにアクセスするだけです。

 

今回の機能追加により、記録カードのモデルクラスには、各項目でのソート機能が追加できました。少し前に検索機能も追加していますから、基本的な機能として、検索とソートが揃ったことになります。他に、ファイルへの保存や、タブ区切りテキストの生成もあり、最低限の機能は満たせたと思います。

あとは実際のアプリで使ってみて、不足する機能を追加するだけです。十分に使い物になると判断していますから、積極的に使いたいと思います。過去に作ったアプリでも、機能追加の際に、このモデルクラスと置き換えてみようかと考えています。

ライブラリ化したモデルクラスは2つに増えましたから、3つ目のモデルクラスとなるモデル候補を物色中です。もし変わったモデルクラスが出来上がったら、また紹介したいと思います。

 

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

2015年7月1日水曜日

共通モデルのライブラリ化を試作(内部変更編)

共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話を紹介しました。今回は、その後の状況を報告します。

 

記録カードのモデルクラスが一段落したので、2つ目のモデルクラスを作りました。記録カードと少し似ている、キー付きカードのモデルクラスです。記録カードにキーが追加されたと考えて、ほぼ間違いありません。記録カードはArreyに相当し、キー付きカードはDictionaryに相当すると考えれば、中身を想像しやすいと思います。

記録カードを作った経験があるので、テストコードも含めて、おおよそ1/3の時間で作り終わりました。クラスに限らず何かのソフトを作る場合には、コーディングしている時間よりも、仕様を考えている時間のほうが多いです。

2つ目のキー付きカードは、記録カードという前例があります。そのため、どんな形で内部データを保存するのか、どんなメソッドがあればよいのか、どんな検査機能を付けなければならないのか、などで悩みませんでした。また、テストコードも似た形で作れるので、その点でも悩みませんでした。まさに、スイスイと作れたという感じです。

肝心なのは、内部データの持ち方ですね。最初からAnyかAnyObjectと決めていたので、どちらにするか決めるだけです。4つの基本データ型だけでなく、将来はクラスのインスタンスも持てるようにしたいはずです。ほとんどのクラスがNSObjectのサブクラスですから、それと相性の良いAnyObjectが適しているでしょう。というわけでAnyObject配列を選びました。

キー付きというモデルなのも関係しているのか、記録カードのモデルクラスよりも、少しスッキリした内部構造に仕上がりました。AnyObject配列を選んで正解だったようです。

 

2つ目のモデルクラスの結果が良かったので、記録カードのモデルクラスも、内部データを持つ配列をAnyObject型に変更してみました。

今までは、データ型ごとに配列を用意したので、配列が4つもあります。それを、1つのAnyObject配列に置き換えるだけです。余計なインデックス変換も不要になり、コードが少し短くなります。

変更前のモデルクラスは別名で保存しておき、変更作業を進めました。実際に作業してみると、30分ぐらいで終了です。内部の仕様だけが変更なので、テストコードはそのまま使えます。コンパイルエラーを全部消して実行すると、一発で通りました。変更完了です。

よく考えると、変更する箇所は多いものの、変更の内容自体は単純でした。4つの配列を消して、1つのAnyObject配列を追加します。インデックスを変換している箇所では、変換している1行を削除し、新しいインデックスに置き換えるだけでした。また、AnyObject配列からデータを読む箇所では、それぞれのデータ型へキャストする指定を追加します。

このようにパターン化できる作業しかないため、かなり慎重に進めても、30分ぐらいの時間で終わりました。当然ですが、一番最初に作ったのは、新しいAnyObject配列の追加と、インデックスの使い方を説明するコメントです。このコメントを見ながら作業するので、間違いを起こさず更新できたのでしょう。

 

そのコメント行を紹介します。まずは、変更前のコメントから。

// ======================================== 3つのインデックスがある
// RecIdx:レコードの並び順を示すインデックス
// FldIdx:項目(フィールド)の並び順を示すインデックス:この値からDataIdxを得て、それでデータへアクセスする
// DataIdx:データ配列内で、どの行(位置)に入っているかを示すインデックス:FldIdxをキーにして、cAryDataIdxから得る
// ============================== 次のように利用する
// cAryName[FldIdx] -> String (項目名)
// cAryType[FldIdx] -> ZmType (データ型)
// cAryDataIdx[FldIdx] -> DataIdx
// cAryDataXXX[DataIdx][RecIdx] -> Data (項目値:4種類のデータ型)
// ========================================

インデックスを変換するDataIdxが使われています。続いて、変更後のコメントです。

// ======================================== 2つのインデックスがある
// RecIdx:レコードの並び順を示すインデックス
// FldIdx:項目(フィールド)の並び順を示すインデックス
// ============================== 次のように利用する
// cAryName[FldIdx] -> String (項目名)
// cAryType[FldIdx] -> ZmType (データ型)
// cAryDataAny[FldIdx][RecIdx] -> AnyObject (項目値:中身は4種類のデータ型)
// ========================================

3つのインデックスから、2つのインデックスへと変更されたのが、もっとも大きな点でしょう。DataIdxがなくなり、FldIdxで直接指定できる形になりました。また、項目値のデータ型が、変数上はAnyObjectだけに変わっています。

 

変更したメソッドのうち、主なものも紹介します。まずは、getメソッドから。

// 値の取得(変更前)
func getDataIntF(rRecIdx:Int, _ rFldIdx:Int) -> Int {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDI:項目定義のデータ型がIntでない"); return -999 }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDI:レコード位置が範囲外"); return -999 }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iInt: Int = cAryDataInt[iDataIdx][rRecIdx]
    return iInt
}
// 値の取得(変更後)
func getDataIntF(rRecIdx:Int, _ rFldIdx:Int) -> Int {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDI:項目定義のデータ型がIntでない"); return -999 }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDI:レコード位置が範囲外"); return -999 }
    let iInt: Int = cAryDataAny[rFldIdx][rRecIdx] as! Int
    return iInt
}

変更後の処理では、インデックスを変換している行が消えて、メソッドが1行分だけ短くなりました。ソースコード全体を眺めても、たいして短くならなかったというのが感想です。内部構造としては、余計なインデックス変換がなくなった点が一番大きいでしょう。

AnyObject型の配列cAryDataAnyから、データを取り出す際にはキャストが必要です。でも、キャストのために数文字を追加するだけですから、あまり変わっていない印象ですね。

getメソッドと違って、setメソッドは変更ありません。Intなどのデータ型の値を、AnyObject配列へ入れるだけですから、特別な指定は不要です。

 

続いて、データを保存する配列の初期化です。

// データ用の配列の初期化(変更前)
// 項目ごとに配列を生成して、その位置(インデックス番号)を記憶する
private func setupArrayF() {
    for i in 0..<cFldCount {  // i=FldIdx
        switch cAryType[i] {
        case .ZmInt :
            cAryDataIdx.append(cAryDataInt.count)  // count値がインデックス値より1つ大きいので、作成前に保存する
            cAryDataInt.append([Int]())
        case .ZmFloat :
            cAryDataIdx.append(cAryDataFloat.count)
            cAryDataFloat.append([Float]())
        case .ZmBool :
            cAryDataIdx.append(cAryDataBool.count)
            cAryDataBool.append([Bool]())
        case .ZmStr :
            cAryDataIdx.append(cAryDataStr.count)
            cAryDataStr.append([String]())
        }
    }
}
// データ用の配列の初期化(変更後)
// データ保存用の配列を初期化して、項目ごとに配列を生成する
private func setupArrayF() {
    cAryDataAny = [[AnyObject]]()      // データ保存用の配列を初期化
    for i in 0..<cFldCount {  // i=FldIdx
        cAryDataAny.append([AnyObject]())    // 項目ごとに、空の配列を追加
    }
}

保存場所を指すインデックス値DataIdxを保存する必要がなくなったのと、配列が1つになったので、かなり短くなりました。ちなみに、これだけ短くなったのは、ここの1箇所だけです。

 

このような感じで、無事に内部データをAnyObject配列に更新できました。

ただ更新しただけでは、とくにメリットはありません。しかし、内部配列がAnyObject型になったことで、いろいろなデータ型を簡単に入れる準備ができました。4つのデータ型とは違うデータ型へも、苦労せずに対応できそうです。

もちろん、以前の内部配列の構造でも、新しいデータ型へは対応できます。AnyObject配列を追加して、そこへ入れれば良いだけです。でも、データ保存用の配列が少しずつ増えていくのは、あまり良い形とは言えません。スッキリした構造から、どんどんと離れていきますから。

今回の変更は、将来の機能拡張の準備として、大きな意味があったと思います。今後も、このモデルクラスを改良して、少しずつ良くしていく予定です。

 

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