2015年5月27日水曜日

ライブラリ等の命名ルールを再考

このブログで紹介しているように、いろいろなアプリで共通に使える機能を、少しずつ独自ライブラリ化しています。ライブラリの数が増えてきて、少し気になっていることがあります。今まではあまり考慮していなかった、命名ルールのことです。

今後もライブラリは増えるでしょうから、あとから区別しやすいように、クラス名などの命名ルールを考え直すことにしました。対象となるのは、ファイル名、クラス名、メソッド名、データ型名、変数名、定義値名などです。何と何を区別すべきなのか、どうしたら簡単に区別できるのか、いろいろと悩んで得た結論を紹介します。

 

まず最初は、何と何を区別するかです。普通に考えて出てくるのは、共通ライブラリとアプリ独自の区別でしょう。

でも良く考えると、ソースコードには他のものも含まれています。Swiftの予約語、iOSのAPIに含まれるクラス名やメソッド名、それ以外のライブラリなどです。これらを、自分が作ったもの(共通ライブラリとアプリ独自の両方)と区別したくなります。つまり、共通ライブラリ、アプリ独自の処理、その他の3つで区別するという結論になります。

この3つのうち、その他だけは、すでに名前が与えられています。自分で名前を変えることはできません。そうなると、共通ライブラリとアプリ独自処理の両方に命名ルールを決めて、その他と区別するのが現実的な方法でしょう。

 

命名ルールで区別する方法としては、参考になる前例がいくつかあります。もっとも有力なのは、名前の先頭に、共通の1文字または2文字を付ける方法です。iOS関連で挙げると、NSで始まる一連のクラス名でしょう。NSはNextStepの略で、iOSのもととなったNextStepのクラスから続いています。というわけで、私も同じように、先頭に共通の文字をつけて区別することにしました。

すでに使っているのは、zという文字です。共通ライブラリとして作ったクラスのインスタンスを入れる変数の一部では、先頭にzを付けています。日付関連ならzDate、ファイル関連ならzFilesと。zを選んだのは、zで始まる英語の言葉が少ないのが理由です。

区別するのは2つですから、文字がもう1つ必要です。いろいろ悩んだ結果、やはり言葉の数が少なそうなyを選びました。つまり、共通ライブラリにはzを、アプリ共通の処理にはyを使うということです。

 

具体的な文字が決まったので、命名ルールを順番に決めていきましょう。

 

まずはクラス名から。クラス名は大文字で始まるため、共通ライブラリでは、大文字のZを先頭に付けます。例えばPhotoSelectorなら、ZPhotoSelectorとするわけです。先頭のZの次も大文字にすることで、先頭のZが、名前とは違う特別な文字だと伝わりやすいでしょう。アプリ独自で使うクラスも同様に、先頭に大文字Yを付けます。

これらのクラスが混在すると、変数宣言でクラス名が現れたとき、先頭がZなら独自ライブラリのクラス、先頭がYならアプリ独自のクラス、その他はそれ以外のクラスと区別できます。それ以外にも先頭がZやYのものがあるので、絶対とは言えませんが、ほとんど問題にならないでしょう。

 

続いて、クラスのインスタンスを入れる変数名です。今まで、共通ライブラリでの変数名の一部には、先頭に小文字のzを付けていました。前述のzDateやzFilesです。この命名ルールのままで構わないので、これからも継続します。また、今まで付けていなかった変数名でも、先頭にzを付けるように変更します。

問題は、アプリ独自のクラスです。そのインスタンスを入れる変数名だけ、先頭に小文字のyを付けるのはどうでしょうか。たとえばyProductとか。確かに区別はできるものの、使いやすいかどうかは試してみないと分かりません。とりあえず試してみて、良かったら続け、さほど良くなかったら命名ルールなしに戻したいと思います。

 

続いて、メソッド名です。これは、クラスのインスタンスを入れた変数名と一緒に使います。その変数名が区別を含んでいるため、わざわざ区別を入れても、二重の情報になってしまいます。というわけで、特別な命名ルールは使いません。

 

今度は、enumなどに付けるデータ型の名前です。enumでデータ型を用意すると、決められた値しか入れられませんから、値のエラーチェックも不要になり、さらに読みやすさも向上したコードが作れます。大変便利で良く使っています。

今までは、あまり気にせずに名前を付けてきました。もしかしたら、似たような名前を作っているかも。また、これから作りそうな気もします。重複しないとともに、どこに所属しているのか明らかなほうが良いと考えました。所属する場所のほとんどは、それを使うクラスです。クラスとの関連性を示せたら良いでしょう。

そこで命名ルールは、クラス名の略号を数文字で表し、先頭に付けることにしました。その数文字ですが、最初の1文字だけが大文字で、それ以外は小文字にします。たとえば、ZPhotoSelectorクラスならZpsと略せ、それをデータ型の名前の先頭に付けます。もしデータ型の名前がZpsBtnTypeなら、ZPhotoSelectorクラスで使われている、ボタンのタイプを意味すると推測できます。Zで始まるため、ライブラリのクラスであることも分かります。

アプリ独自のクラスでも、同じ命名ルールを採用しましょう。Yで始まる数文字の略語を得て、データ型の先頭に付けます。

 

続いて、定義値の名前です。定義値というのは、最大値や固定値などに名前を付けて定義し、処理の外に出して、見つかりやすくするものです。後から変更しそうな値に適用します。C言語の時代から使われていて、すべて大文字で表現する名前がよく用いられます。

私の場合だけかもしれませんが、処理の外に出す目的以外でも使っています。共通で使うような値を、定義値として最初に用意し、アプリ全体で使ったりします。定義値を用意することで、名前による間接参照にもなりますし。

このような使い方なので、使う機会はかなり多いです。作った場所というか、持ち主が区別できると良いと思っています。

その命名ルールですが、ライブラリの場合は、先頭にZ_を付けることにしました。名前の全部が大文字なので、Zだけだと区切りが不明確になるからです。今までCOLOR_ERRORとしていたのを、今後はZ_COLOR_ERRORとします。

もう1つとなるアプリ独自の定義値の名前は、何も付けないことにしました。その他の分類で似たような定義値が使われていないようなので、ライブラリだけ区別すれば十分と考えたからです。先頭にZ_が付いていればライブラリの定義値、それ以外はアプリ独自の定義値と判断します。

 

最後は、ソースコードのファイル名です。これは以前に「管理しやすい分類情報付きファイル名を使おう」にて紹介しました。あれから分類が1つ増えたので、今では次のようになっています。

// ファイル名の先頭に付ける主な分類情報
・AC:特定の条件(装置など)に依存する共通ライブラリのクラス(複数アプリで利用)
・BP:共通ライブラリの関数や定義値など(複数アプリで利用)
・BV:共通ライブラリのクラスでUIView型(複数アプリで利用)
・BC:共通ライブラリのクラスでUIView型以外(複数アプリで利用)
・MC:アプリで使うモデルのクラス
・VC:アプリで使うビューのクラス

このような形でファイル名を付けると、ファイルの数が増えても大まかに分類できます。最初の2文字が大分類、その次の言葉が中分類、さらに次の言葉が小分類という形で、それぞれの役割が簡単に推測できてしまいます。非常に便利に使っているので、今のままの命名ルールで続けます。

 

今回の考察によって、ライブラリとアプリ独自のコードが、かなり区別できるようになるはずです。ライブラリ内の名前の変更が必要ですが、後になるほど大変なので、今やっておくしかないでしょう。少しずつ更新しようと思います。

もう1つ、作らなければならないものがあります。上記のルールを書いた書類を用意して、ライブラリを使っているアプリにも入れることです。そうしておくと、私以外の人がアプリをメンテするとき、ソースコードを解読しやすくなるでしょう。私が永遠にメンテできるはずはないので、次の人への準備は非常に重要だと思います。

アプリをビルドしたとき、余計な書類が含まれていてもアレなので、ルールを書いた書類はSwiftのソースコードが良いかもしれませんね。すべてをコメントとして書き、ファイル名にZZ_to_Developerとか付けて。メンテする人が、必ず見てくれるように。

 

さっそく、今作成中のライブラリから適用しています。これまでのライブラリやアプリも、バージョンアップの際に更新する予定です。

2015年5月21日木曜日

iOS実験専用アプリを活用しよう(5)

かなり前になりますが、「iOS実験専用アプリ」の作り方と使いかたを紹介しました。今でも使い続けていますが、以前よりも使い方が上手になっています。というわけで、まだ公開していない新しい使い方を、続編として紹介します。

 

このアプリにはボタンが40個もあり、それぞれに別な機能を割り当てられます。割り当てられた機能の数が増えるほど、画面上に加えるUI部品の数も増えます。そのままでは使いづらいので、使い終わったボタンの機能は、オフする方法も紹介しました。ボタン名の最初に「x」を付けて、ボタンに割り当てた機能をコメント化する方法です。

この方法で問題なく使えるのですが、やはりオンオフが面倒だと感じるようになりました。オフした機能を使う際に、今使っている機能をオフすることも多く、切り替え作業は意外に面倒なのです。

というわけで、いちいちオフしなくても構わない作り方はないのかと、深く考えてみました。やはり真剣に考えると、思い付くものなのですね。今までよりも格段に良い作り方が、できてしまいました。

 

新しい作り方を紹介する前に、以前の作り方を簡単に説明します。

各ボタンには、2つの関数が用意されています。初期化の際に呼ばれるstartBtnXXF関数(XXは2桁の数字)と、ボタンをタップした時に呼ばれるtestBtnXXF関数(XXは2桁の数字)です。

通常の作り方は、初期化のstartBtnXXF関数でUI部品を表示し、ボタン用のtestBtnXXF関数で何かを実行するという形式でした。初期化の際にUI部品を表示してしまうため、複数のボタンをオンすると重ねて表示され、一緒にオンすることができなかったのです。

 

この問題を解決するために、少し発想を変えました。ボタン用のstartBtnXXF関数に初期化の処理も含めたら、オフする必要がないのではないかと。つまり、使い始めるまで何も表示させないという作り方です。

それとともに、使い終わったときの状態も考えなければなりません。使い終わってUI部品が表示されたままだと、別なボタンの機能を使う時に邪魔になります。そうならないようにと、画面上をクリアーする機能も、ボタンの機能に加えることにしました。

ただし、これらの機能を複数のボタンに割り当てたら、ボタンをどんどん消費してしまい、40個もあるボタンがすぐに足りなくなります。そこで、1つのボタンだけ使って、初期化からクリアーまでを実行できる方法を考えました。

解決策は簡単でした。タップされるたびに、順番に実行していけば良いのです。最初のタップで初期化だけします。次のタップでは、1つ目の使い方を実行します。さらに次のタップでは、2つ目の使い方を実行します。このように続けて、一番最後のタップにクリアー処理を割り当て、画面上のUI部品を消します。

クリアー処理が終わると、最初の状態に戻します。そのまま同じボタンをタップすれば、2回目の初期化処理が始まります。このように、初期化からクリアー処理まで何度も繰り返せるように作れば良いのです。

 

以上のような説明だと、難しそうに感じたかもしれませんが、実は意外なほど簡単です。具体的なSwiftコードを紹介しましょう。単純な例のほうが理解しやすいので、2種類の使い方しか含まない例を。

// ======================================== ボタン17:棒グラフ
var graphView17: GraphBarView!
var num17: Int = 0

func startBtn17F() {
    aryButton[17].setTitle("棒グラフ", forState: .Normal)
}
func testBtn17F(rSender:UIButton) {
    setMsgF(1, "タップ17")
    run17F()
}
func run17F() {
    switch num17 {
    case 0:
        setMsgF(2, "生成")
        graphView17 = GraphBarView()
        graphView17.setupF(10, 10, 600, 400)
        graphView17.setLabelF(["SEP 1","SEP 2","SEP 3","SEP 4","SEP 5","SEP 6","SEP 7"])
        graphView17.setDataF([1,24,12,18,30,10,21])
        graphView17.setDataRangeF(0, 40)
        viewBase.addSubview(graphView17)
        graphView17.drawGraphF()
        num17 = 1
    case 1:
        setMsgF(2, "正常1")
        graphView17.setLabelF(["NOV 1","NOV 2","NOV 3","NOV 4","NOV 5","NOV 6","NOV 7"])
        graphView17.setDataF([11,4,22,28,3,20,1])
        graphView17.setApparanceF(1)
        graphView17.drawGraphF()
        num17 = 2
    case 2:
        setMsgF(2, "正常2")
        graphView17.setLabelF(["SEP 1","SEP 2","SEP 3","SEP 4","SEP 5","SEP 6","SEP 7"])
        graphView17.setDataF([1,24,12,18,30,10,21])
        graphView17.setApparanceF(2)
        graphView17.drawGraphF()
        num17 = 999
    case 999:
        setMsgF(2, "終了")
        graphView17.removeFromSuperview()
        num17 = 0
    default:
        break
    }
}

見てのとおり、値ゼロで用意した変数num17を使って、順番に実行させているだけです。要点だけ簡単に説明します。

同じボタンを何度もタップするので、何の処理をしているのか画面上に示す必要があります。この例では、メッセージ欄に短い文字列を表示しています。最初が「生成」で、UI部品を画面上に表示させます。続く「正常1」は、正常系の1番目の実行です。この例には含まれてませんが、エラー系(エラーを含んだ処理)も後に付けるため、通常の使い方を「正常」と表示しています。次の「正常2」も正常系で、別な内容を実行します。最後の「終了」がクリアー処理で、画面上からUI部品を消し、変数num17を初期状態のゼロに戻します。

「終了」してしまうと、メッセージ欄の文字列を除いて、画面上には何も残りません。このボタンの機能を繰り返し実行しても構いませんし、別なボタンの機能を実行することもできます。他のボタンの機能を続いて試す場合は、「終了」で終わらせるのが基本的な使い方です。

 

実行させたい機能は、正常系だけではありません。ほとんどの機能にはエラー処理も含まれているので、正常系に続いて、エラー系の実行もボタンに含めます。前述の例にエラー系の実行を1つだけ加えたSwiftコードは、次のようになります。

// 前述のコードに、エラー系の実行を1つ加えたもの
func run17F() {
    switch num17 {
    case 0:
        setMsgF(2, "生成")
        graphView17 = GraphBarView()
        graphView17.setupF(10, 10, 600, 400)
        graphView17.setLabelF(["SEP 1","SEP 2","SEP 3","SEP 4","SEP 5","SEP 6","SEP 7"])
        graphView17.setDataF([1,24,12,18,30,10,21])
        graphView17.setDataRangeF(0, 40)
        viewBase.addSubview(graphView17)
        graphView17.drawGraphF()
        num17 = 1
    case 1:
        setMsgF(2, "正常1")
        graphView17.setLabelF(["NOV 1","NOV 2","NOV 3","NOV 4","NOV 5","NOV 6","NOV 7"])
        graphView17.setDataF([11,4,22,28,3,20,1])
        graphView17.setApparanceF(1)
        graphView17.drawGraphF()
        num17 = 2
    case 2:
        setMsgF(2, "正常2")
        graphView17.setLabelF(["SEP 1","SEP 2","SEP 3","SEP 4","SEP 5","SEP 6","SEP 7"])
        graphView17.setDataF([1,24,12,18,30,10,21])
        graphView17.setApparanceF(2)
        graphView17.drawGraphF()
        num17 = 101
    case 101:
        setMsgF(2, "エラー1")
        println("----------TEST-ERROR-1:Apparanceが範囲外")  // 区切り行
        graphView17.setLabelF(["NOV 1","NOV 2","NOV 3","NOV 4","NOV 5","NOV 6","NOV 7"])
        graphView17.setDataF([11,4,22,28,3,20,1])
        graphView17.setApparanceF(9)    // エラー
        graphView17.drawGraphF()
        num17 = 999
    case 999:
        setMsgF(2, "終了")
        graphView17.removeFromSuperview()
        num17 = 0
    default:
        break
    }
}

エラー系が正常系と違うのは、メッセージ欄に「エラー1」と出すだけではありません。続くprintln文で、エラーメッセージの区切り行を出力しています。区切り行には、発生が予想されるエラー内容を、短い言葉で書くのが基本です。

エラー系の実行では、処理内容に何らかのエラーを含めるため、実行対象の機能からエラーメッセージが表示されます。そのメッセージが、エラー系のどの実行で出されたメッセージなのか、判別できる必要があります。その判別を容易にするために、区切りのメッセージを出力するわけです。

区切り行だという点を強調するために、先頭にハイフン10個を付けています。エラーメッセージと一緒に並んだとき、区切り行として見えるようにとの配慮です。具体的には、次のように出力されます。

----------TEST-ERROR-1:Apparanceが範囲外
2015/05/20 20:24:50 ERROR:GBV_SA:Apparance番号が範囲外

これだけだと効果を実感できませんが、複数のエラー実行が並んでいて、1つの実行で複数のエラーメッセージが出力されたとき、区切り行のおかげで見やすく整います。

 

ちなみに、変数num17に設定する値ですが、単純な連番ではありません。この例では、正常系は1から始まり、エラー系は101から、終了は999にしてあります。このように作ってあるのは、正常系やエラー系の実行内容を追加したとき、変更する箇所を減らすためです。

もし最初から最後まで連番の場合、修正が大変になります。たとえば、正常系が15個、エラー系が20個あったとき、単純な連番だとエラー系は16番から始まります。そこへ。正常系の処理を1つ追加する場合、エラー系の開始番号が16番から17番へと変更になり、後に続く番号をすべて変えなければなりません。そんな無駄を避けるために、飛び飛びの番号をつけているのです。

この例では正常系の番号を1つずつ増やしていますが、2ずつ増やすとか、10ずつ増やすとかでも構いません。後から追加するとき、間に挿入しやすくなりますから。別な考え方として、正常系の実行を複数グループに分け、最初のグループは1番から始まり、次のグループは21番から、さらに次のグループは41番から、という形で番号を付ける方法もアリです。

 

今回は、「iOS実験専用アプリ」の上手な使い方として、機能をオフしなくて済むような作り方を紹介しました。

この方法が適さない機能もありますから、必要に応じて使い分けたり、別な新しい方法を考え出したりする必要があります。工夫して使うほど、iOS実験専用アプリはどんどん便利になりますね。私の場合、もはや必須ツールとなっています。

 

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

2015年5月18日月曜日

ライブラリ化とInterface Builder不使用の重大な関係

先日の連休中に、友人のIT技術者と、じっくり話をしながら飲みました。そのときに、ライブラリ化の話で盛り上がったのですが、私が気付いてなかった点を指摘されました。その話を短かく取り上げます。

 

友人の指摘は「ライブラリ化が容易になっているのは、Interface Builderを使わずに開発していることが、非常に大きい」というものでした。ぜんぜん気にしていない点だっただけに、かなり驚きました。

友人の意見は大事な点が何個も含まれていましたが、要旨を整理すると以下のようになります。

友人の意見の要旨
・Interface Builderを使わないことで、
 ・ライブラリに含まれるのはSwiftのソースコードだけ
 ・サイズの変更が容易となり、Viewの大きさに合わせたサイズ調整が容易
 ・必要な数だけのUI部品が、短いソースコードだけで連続して生成できる

この中で一番大きな点は、Swiftのソースコードだけでライブラリが作れる点だというのです。

 

このブログの最初に宣言したとおり、私の開発ではInterface BuilderとStoryboardをまったく使っていません。UI部品の生成関数を用意して、すべてのUI部品をその関数で生成しています。そのため、ライブラリに含まれるのは、Swiftのソースコードだけとなります。まさに、指摘されたとおりです。

どのライブラリも、Swiftのソースコードだけで作られていて、しかも1つのファイルに収まっているため、アプリへは簡単にコピーして持っていけます。

当然ですが、アプリ内でのライブラリのバージョンアップも簡単です。古いソースコードを削除して、新しいソースコードに入れ替えるだけです。ライブラリのインターフェースはめったに変えませんから、アプリ側を直さずにバージョンアップできています。Swift側やAPI側の仕様変更を除いて。

 

ライブラリを作るときだけでなく、テスト環境まで保存しているiOS実験専用アプリ自体も含めて、Interface BuilderやStoryboardを使っていません。Xcodeの新規プロジェクトから、Single View Applicationを選び、空の画面のアプリケーションを用意しました。そのままSwiftコードだけを追加して、iOS実験専用アプリを仕上げています。

当然ですが、ライブラリのテストで使う画像や音のファイルは、iOS実験専用アプリのプロジェクトに追加しています。それ以外のテキストデータなどは、Swiftのテストコードに中に入れ、ソースコードの形で用意できるようにしています。テスト用のテキストファイルも基本的に、Swiftのソースコードから生成し、わざわざファイルを用意しないのが基本です。

このように、改めて見直すと、できる限りSwiftのソースコードから作っていますね。今書いていて気付いたのですが、別に用意している画像ファイルも、描画機能を使って描いてからファイルに保存すれば、Swiftのソースコードから作れますね。まあ、やらないでしょうけど。

 

Swiftでアプリ開発を始めるとき、ツールによる余計なトラブルを避ける理由と、自由度を優先するという理由から、Interface BuilderやStoryboardを使わないと決めました。その選択が結果として、ライブラリ構築の自由度も上げていたわけです。

自分にとっては、あまりに当たり前の選択だっただけに気にしておらず、言われるまで気付きませんでした。結果としては、Interface BuilderとStoryboardを使わない選択が、ライブラリ構築にも良かったわけです。

ちなみに友人も、Interface BuilderやStoryboardを使わない方式に切り替えたそうです。そして、自分専用の独自ライブラリを少しずつ構築中だとか。

 

とは言うものの、Interface BuilderやStoryboardを使わない開発を、他の人に勧めるかといえばノーですね。開発者にもいろいろな人がいて、便利ツールがないと苦労する人もいますから。ですから最初は、とりあえずInterface Builderを試すように勧めます。

Interface Builderを使わずに開発する方法には、軽い壁があると思います。それほど大きな壁ではありませんが、それを乗り越えられる人でないと難しいでしょう。具体的には、UI部品を生成するライブラリを自分で整えるとか、Interface Builderの代わりとなる道具を自分で用意する手間と技術が必要となります。その条件を軽く乗り越えられる人なら、Interface Builderを使わなくても大丈夫です。

もし大丈夫な人なら、使わないほうを積極的に勧めます。ライブラリさえ用意してしまえば、開発の自由度が格段に増し、すべてソースコードでコントロールできる形になりますから。経験が長いというか、それなりに実力のある人には、使わないほうを積極的に勧めたいですね。

 

Interface Builderを使わない開発では、画面レイアウトの問題が常に生じます(使う開発でもある程度は生じていますが、自動レイアウトで何とかなる場合もありますから、問題が軽減されているでしょう)。

今のところiPad開発ばかりなので、画面サイズの違いを考慮しなくて済んでいます。UI部品の表示位置や大きさを数値で指定する方法で、問題はまったくありません。しかし今後はiPadも、複数の画面サイズを意識しなければならない状況になると予想します。現状のiPhoneのように。

一応、対策は考えています。自動レイアウトは使いたくないので、画面サイズごとにレイアウトを切り替える予定です。切り替えといっても、ソースコードは1つにして、画面サイズごとに別な座標数値を使う方法です。座標数値を配列に入れ、その配列を参照しながら表示位置を決めれば、複数のレイアウトへ簡単に対応できます。UIView型ライブラリがサイズ可変なのも、このような状況を予想してのことです。配列から座標を読み込む方法で作った結果、見栄えや使い勝手が悪い場合にだけ、完全に別なレイアウトで作る予定です。

複数の画面レイアウトの問題は、Interface Builderで解決できるものではありません。自動レイアウトも限界はありますし、細かな見栄えは低下しがちです。今後も、大きな課題として残り続けるでしょう。

 

画面レイアウトの問題があっても、Interface BuilderとStoryboardを使わない開発を、止める気はありません。今までどおり、Swiftのソースコードだけで作った独自ライブラリを、どんどんと増やしていくつもりです。

Swiftでの開発に慣れてきて、Interface Builderを使っている方は、使わない方法を一度検討してみてはいかがでしょう。もしかしたらですが、素晴らしい未来が待っているかもしれません。最初の取り掛かりとして、UI部品を生成するライブラリ作りからお勧めしたいですね。

2015年5月15日金曜日

フォトアルバムから画像取得のライブラリ化を試作(将来の仕上げ編)

フォトライブラリから画像取得の独自ライブラリを試作した話の補足です。試作したソースコードを、最後に仕上げるときの考慮点について少し書きます。

 

当ブログの投稿に何度も書きましたが、ライブラリ化する際に大事な考慮点があります。複数のアプリで使えることに加え、似たような機能と交換可能に作ることです。将来の仕上げでは、それらの点も考慮しなければなりません。

複数アプリで使える点は、独立したクラスとして作ったことで実現しました。似たような機能と交換可能な点は、現状よりも深く考えて決める必要があるでしょう。

 

まず、似たような機能を考えてみます。やはり写真を取得する機能でしょう。真っ先に思い浮かんだのが、フォトライブラリ以外から取得する機能です。iPad内からの取得だけでなく、インターネット経由で取得する機能も考えられます。これらの機能に変えたり、切り替えて使う状況も考慮しておくのがベストです。

今回試作したクラスは、UIViewのサブクラスとして作りました。画面に表示する機能も含まれているため、画面上の操作や反応もクラス内で記述し、その部分はアプリと無関係に作れるので、アプリからの独立性が高いと言えます。そのため試作クラス側では、アプリを気にせずに、表示内容や操作方法の変更が可能です。

逆にアプリに関係する点は、最低限の内容になっています。画面に表示するサイズ、画像を選択中かの問い合わせ、画像の取得ぐらいです。このような形なので、別な機能と交換するのも容易になっています。

そもそも私がライブラリ化する際には、画面に表示して操作する機能の場合、UIViewのサブクラスを選択しています。その最大の理由は、ライブラリ化した側に表示や操作を作れるからなのです。もう1つ、表示サイズを可変にしているのも、交換しやすいようにと考えてのことです。今回の試作クラスも同じ方法を選んだため、予想どおり結果になったというわけです。

 

というわけで、似た機能との交換可能性を上げるためには、メソッドを整えるのがキモとなります。順番に見ていきましょう。

初期化では、画面上の表示サイズを指定しているだけですから、問題ありません。もちろん最小サイズの違いはありますが、大きく使う分には同じに使えます。もし小さいサイズで使う場合は、画面上のレイアウトで対応するしかないでしょう。

取得画像のデフォルトサイズ設定メソッドでは、縦横の大きさを指定しています。この大きさの意味、つまり指定した大きさの値が、どのように使われるのかは、交換可能な機能を実現するためのAPIに依存します。できれば最大サイズを指定しているという意味にしたいのですが、それが可能とは限りません。仕方がないので、最大サイズに近い画像を得られるように、それぞれの実現方法で工夫するしかないでしょう。

最大サイズ以外の指定方法を考えてみても、代わりとなる値の種類は思い付きませんでした。現実的な選択肢としては、現状のサイズ指定がベストだと思います。

続いて、画像の取得メソッドは2種類を用意しています。デフォルトのサイズ設定で画像を取得するメソッドは、そのままで構わないでしょう。もう1つのサイズ指定による画像取得メソッドも、サイズ指定の方法が現状でベストとなれば、こちらも同じで構わないはずです。

取得が失敗した際にnilを返す方法も、そのまま別なクラスで使えます。ただし、失敗した理由をユーザーに伝える方法は、何かしら用意しなければなりません。クラス内にUIViewを持ってますから、メッセージ欄に表示するのが現実的だと思います。

取得関係では他に、画像情報を取得するメソッドも試作クラスで作りました。これはPhotosフレームワークに依存するデータ型なので、似た機能と交換可能にはなりません。また画像情報からUIImageを得る変換メソッドも同様です。これら2つのメソッドは、今回の試作クラス独自の拡張機能という位置付けにして、可能な限り使わない方針に決めます。特別な理由がない限り使わないメソッドという扱いです。

最後は、画像が選択中かどうか問い合わせるメソッドです。こちらは、Bool型で返す形式なので、何も問題はないでしょう。

 

以上は試作クラスで用意したメソッドですが、他に必要となる共通メソッドはないか、そもそもメソッドを考える前に、他に必要な共通機能があるかも検討しなければなりません。

おそらく必要な機能としては、交換可能な似たクラスの内容により、それぞれで出てくると予想します。たとえば、インターネット経由で画像を取得するクラスなら、通信状況を表示する機能が必要でしょう。ただし、共通の機能ではないため、今回の検討からは外れます。

交換可能なクラスで共通する機能となると、残念ながら思い付きませんでした。とりあえずは、現状のままで構わないと判断しました。

 

今回のような検討(似たような別機能への交換可能性を考慮して作る検討)は、試作している途中のどこかで必ず行います。

使用するAPIの仕様が十分に見えていて、どのように作るのかが見通せる場合は、最初の全体設計の段階で検討できます。できるだけ早めに、検討したほうが良いでしょう。

しかし、初めて使用するAPIの場合は、詳細を知らない段階なので、検討しても後から変更する可能性が高まります。試しに作りながら、APIの仕様が大まかに見えてきた段階で、検討するのが効率的です。

実際には、中心となるメソッドを決める場合に、できるだけ汎用的な形式を選ぶようにしています。また、UIViewのサブクラスで作るなど、アプリから独立した形になる工夫も採用しています。そのため、メソッドの選択を大きく間違うことはありません。

全体として、気付いたら修正するという流れで構わないと思います。

 

似た機能と交換可能に作るという考え方は、変更への柔軟性を高めるための重要な点だと思います。ここで紹介したような検討を、作る際に試してみてはいかがでしょう。

2015年5月11日月曜日

フォトアルバムから画像取得のライブラリ化を試作(本編2+使用例編)

フォトライブラリから画像取得の独自ライブラリを試作した話の続きです。Swiftコードの紹介が途中までだったので、その続きから始めます。

 

2つのTableViewを取り上げたので、残りのUI部品を紹介します。UIViewの右側ブロックに入れる部品で、画像表示用のUIImageViewと、メッセージ欄のUILabelが含まれます。初期化処理から呼ばれて、画面上に表示するためのSwiftコードです。

// ======================================== 他のUI部品
var msgLabel: UILabel!
var imgView: UIImageView!

private func setupUiPartsF() {
    // メッセージ&情報表示用ラベル
    let iX: CGFloat = W_MARGIN_LEFT + W_NAME_TABLE + W_MARGIN_CENTER + W_NAME_TABLE2 + W_MARGIN_CENTER2
    let iY: CGFloat = H_MARGIN_TOP
    let iWidth: CGFloat = w_image
    let iHeight: CGFloat = 25
    msgLabel = zCreateLblF("", 18, ALIGN_CENTER, iX, iY, iWidth, iHeight)
    self.addSubview(msgLabel)
    // 画像表示用ImageView
    let iX2: CGFloat = iX
    let iY2: CGFloat = H_MARGIN_TOP + H_LABEL_W_MARGIN
    let iWidth2: CGFloat = w_image
    let iHeight2: CGFloat = h_image
    imgView = zCreateImgViewF(defaultImg, iX2, iY2, iWidth2, iHeight2)
    imgView.contentMode = UIViewContentMode.ScaleAspectFit // 縦横比保持で最大表示
    self.addSubview(imgView)
}
private func setMsgF(rStr:String) {
    msgLabel.text = rStr
}

他のUI部品と同じように、座標位置と大きさを計算してから、UI部品の生成関数で生成しています。UIImageViewでは、画像を表示する方式も大事です。縦横比を保持したまま最大に表示する方式に設定しました。

メッセージ欄へのアクセスですが、UI部品を直接呼び出す形ではなく、アクセス用の専用関数を付けました。いつも使っている、間接参照によるアクセスです。

 

初期化処理から呼ばれる関数が終わったので、残りの関数に移ります。

今回の肝となる処理の1つでもある、PHAssetからUIImageを生成する関数です。生成する際にはサイズ指定が必要なので、画像の幅と高さも引数に含めています。

// ======================================== PHAssetからUIImageを生成
// 仕様確認の結果:元サイズより拡大することはないようだ(Xcode:6.3.1,iOS-SDK:8.3)
// 仕様確認の結果:指定したサイズは、それぞれ最小サイズの意味らしい。縦横比保持で(500,500)を指定すると(700,500)等で生成される。
// 上記結果からの疑問:オプションの設定値で動作結果が変わるかも。ただし、それらしい設定は見付けられていない。
// SDKのバグ?:サイズ指定の値が小さいと、関係ないサイズで生成される(Xcode:6.3.1,iOS-SDK:8.3)
func convert2ImgF(rPHAsset:PHAsset, _ rWidth:CGFloat, _ rHeight:CGFloat) -> UIImage? {
    if (rWidth < IMG_WIDTH_MIN) { zSendErrMsgF("ERROR:PHV_C2I:画像サイズの幅が不足"); return nil }
    if (rHeight < IMG_HEIGHT_MIN) { zSendErrMsgF("ERROR:PHV_C2I:画像サイズの高さが不足"); return nil }
    var iImage: UIImage? = nil
    let iSize: CGSize = CGSizeMake(rWidth, rHeight)
    let iOptions = PHImageRequestOptions()
    iOptions.resizeMode = PHImageRequestOptionsResizeMode.Fast // 縦横比を保ち、小さい辺が指定サイズで生成する
    iOptions.deliveryMode = PHImageRequestOptionsDeliveryMode.FastFormat   // .FastFormat, .Opportunistic (後で違いを調査)
    iOptions.synchronous = true
    //let iContentMode: PHImageContentMode = PHImageContentMode.AspectFill // 値メモ .AspectFill, .AspectFit
    PHImageManager.defaultManager().requestImageForAsset(rPHAsset, targetSize:iSize, 
        contentMode: .AspectFill, options:iOptions, resultHandler:{ (rImage, rInfo) -> Void in
        iImage = rImage
    })
    return iImage
}

最初に、サイズ指定の値を検査しています。検査に引っ掛かると、何もしないでnilを返します。

続いては、UIImage生成処理で指定するオプションの準備です。PHImageRequestOptionsインスタンスを生成し、必要な値に設定します。

オプション値を説明したSDK資料を見たのですが、詳しい動きが書いてありませんでした。仕方がないので、それらしい値に設定して、結果を表示させながら、上記ソースコードの値が残りました。この中では、deliveryModeに違いが感じられず、後で詳しく調べてみる必要があるでしょう。

オプション以外では、contentModeも指定する必要があります。縦横比を保持するAspectFillを選びました。また、これは前回紹介した工夫と同じように、コメント行で他の値を見つけやすくメモを残しています。

準備ができたらrequestImageForAssetメソッドを実行し、UIImageが生成できればそれを返し、生成できなければnilを返します。

このconvert2ImgFメソッドには、先頭にコメント行が何行も含まれています。実際に実行して気付いた点を、開発ツールのバージョンとともに、メモとして残しました。具体的には、元サイズより大きなサイズを指定しても、元サイズより大きくはならないとか、サイズ指定が最大値の指定ではなく、最小値の指定を意味しているようだとか、サイズ指定の値が小さいと、関係ないサイズで生成されるとかです。これらの点に関しては、仕上げの段階で再確認する必要があるでしょう。

このようにメモすれば、後で開発を再開した時に忘れることがなく、修正漏れが発生する心配がありません。開発時に気付いた点は、その時点で確実にメモすることが大事です。関係するソースコード上が、メモするのに最適な場所でしょう。

 

次は、生成する画像サイズの変更メソッドです。デフォルトの値を持っていますが、好きな値に変更したい場面が確実にあるでしょう。

// ======================================== 画像サイズの設定
func setImgSizeF(rWidth:CGFloat, _ rHeight:CGFloat) {
    if (rWidth < IMG_WIDTH_MIN) { zSendErrMsgF("ERROR:PHV_SIS:画像サイズの幅が不足"); return }
    if (rHeight < IMG_HEIGHT_MIN) { zSendErrMsgF("ERROR:PHV_SIS:画像サイズの高さが不足"); return }
    imgWidth = rWidth
    imgHeight = rHeight
}

処理内容としては、引数の値を検査して大丈夫なら、クラス変数に値を保存しているだけです。

 

続いては、画像を取得するメソッドです。いろいろな使い方を考慮し、3つも作ってみました。

// ======================================== 画像の取得
// 画像の取得
func getImageF() -> UIImage? {
    if isSelected {
        return imgView.image
    } else {
        return nil
    }
}
// 画像の取得(サイズ指定)
func getImageF(rWidth:CGFloat, _ rHeight:CGFloat) -> UIImage? {
    if (rWidth < IMG_WIDTH_MIN) { zSendErrMsgF("ERROR:PHV_GI:画像サイズの幅が不足"); return nil }
    if (rHeight < IMG_HEIGHT_MIN) { zSendErrMsgF("ERROR:PHV_GI:画像サイズの高さが不足"); return nil }
    if isSelected {
        let iImage: UIImage? = convert2ImgF(phAsset, rWidth, rHeight)
        return iImage
    } else {
        return nil
    }
}
// 画像参照の取得
func getAssetF() -> PHAsset? {
    if isSelected {
        return phAsset
    } else {
        return nil
    }
}

並んでる順に紹介します。

最初のメソッドは、ImageViewに表示しているUIImageを、そのまま返すタイプです。このUIImageは、このクラスのインスタンスに保持されているサイズ指定を使って、生成されたものになります。

次のメソッドは、引数にサイズ指定を含み、そのサイズに合わせて新たに生成したUIImageを返すタイプです。目的に合わせた大きさで画像を生成できます。

最後のメソッドは、UIImageではなく、PHAsset(画像参照情報)を返すタイプです。使うかどうかは不明ですが、とりあえず付けてみました。PHAssetからUIImageを生成するconvert2ImgFメソッドも外からアクセス可能にしているため、情報を保持しておけば、好きなときに好きなサイズでUIImageが生成できます。

 

いよいよ、最後のソースコードです。写真が選択されている状態か、アプリ側から判定できるメソッドです。

// ======================================== 選択中の判定
func isSelectedF() -> Bool {
    return isSelected
}

処理内容としては、内部のフラグを返しているだけです。でも、このメソッドは使い勝手に大きく関係しますから、クラスとしては必須です。この判定結果を知れることにより、「先に画像を選んでください」とか、適切なメッセージをアプリが出せるからです。

 

以上で、Swiftコードの紹介は終わりです。続いて、使用例を紹介しましょう。

このクラスの使い方は非常に簡単です。クラスのインスタンスを生成し、初期化してから好きなViewに貼り付けるだけで準備完了です。

// PhotoSelectorクラスを使う
let aPhotoSelector = PhotoSelector()
aPhotoSelector.setupF(10, 10, 900, 400)
viewBase.addSubview(aPhotoSelector)

たったこれだけで、フォトライブラリのアルバムから写真を選べるUIViewが画面上に追加できます。

同様に、選択した画像を取得するのも簡単です。たとえば「登録」ボタンに、以下のような関数を設定します。

// 「登録」ボタンに付ける処理
func addBtnF(rSender:UIButton) {
    if aPhotoSelector.isSelectedF() {
        if let iImg: UIImage = aPhotoSelector.getImageF(1000, 700) {
            //
            // ここに画像の登録処理を入れる
            //
            setMsgF(4, "選択中の画像を登録しました。")
        } else {
            setMsgF(4, "登録に失敗しました。")
        }
    } else {
        setMsgF(4, "先に画像を選んでください。")
    }
}

もし写真が選択中でなければ、選択を促すメッセージが表示されます。選択中であって、UIImageが無事に取得できれば登録処理へ進み、登録完了のメッセージが表示されます。逆にUIImageを取得できないときは、エラーメッセージだけを表示します。

 

以上のような使い方は、PhotoSelectorのUIViewを画面上に貼り付けています。画面上の1要素として、PhotoSelectorを使う形です。

それとは異なり、一時的に画面上に表示して、画像を選ばせるような使い方も可能です。たとえば、画面と同じサイズのUIView(画面を覆うためのUIViewです)を用意して、その中央にPhotoSelectorのUIViewを貼り付けます。また「登録」ボタンや「キャンセル」ボタンも、一緒に用意します。

それら全体を画面上に表示して覆い、画像を選択させます。PhotoSelectorを操作してから「登録」ボタンをタップすると、登録処理を済ませて、覆っていたUIViewごと消します。「キャンセル」ボタンをタップした場合は、何もせずに、覆っていたUIViewごと消します。このような形で作ると、画像選択のモーダルダイアログのようにも使えます。

 

実際に使ってみると、中央のTableViewが使いづらく感じました。意味不明な記号のような文字列(localIdentifierの値)が並ぶだけで、どんな画像なのか不明だからです。シミュレーターで試した表示は、次のようになりました。

使いやすくするためには、中央のTableViewを、文字列ではなく画像表示に変更すべきでしょう。画像表示なので、一般的なTableViewではなく、格子状の並びが適していそうです。その点だけ改良すれば、使い勝手も十分だと思います。アプリ開発が決定した段階で、変更を加える予定です。

ここまでの状態でも、肝となるPhotosフレームワークに関係する部分は、ほぼ作り終わっています。細かな設定を確認する必要はありますが、アプリ開発が決まった時点でも構わないと思います。この段階まで作ってあれば、残りは細かな部分だけなので、短時間でなんとでもなるでしょう。というわけで、試作は一旦終了としました。

 

今回は、独自ライブラリを試作するという形で、完成まで達していない例を紹介してみました。使ったことのない機能の場合、近いうちに使いそうだと感じた段階で、とりあえずの試作を作ってみるのはアリだと思います。十分なテストはしていなくても、一般的な使い方でなら、基本的な機能が動く程度まで作っておけば、準備段階としては十分と言えます。

もちろん試作であっても、いろいろなアプリで使える形に作るのは必須です。最初は簡単な機能で作り、アプリ側とのインターフェースをできるだけ変えずに、機能を少しずつ加えていくのが、上手な作り方だと思います。

 

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

2015年5月8日金曜日

フォトアルバムから画像取得のライブラリ化を試作(本編1)

フォトライブラリから画像取得の独自ライブラリを試作した話の続きです。検討した全体設計に合わせて、Swiftコードを書き進めました。最後のほうは動かしながら少し改良して行ったのですが、結果のソースコードだけ取り上げます。

長いソースコードを一気に乗せるとコピーしやすいのですが、説明が理解しづらくなります。いつものように、ソースコードを短く切り、それぞれに説明を加えていきます。

 

まずは全体の入れ物から。クラス名は「PhotoSelector」と付けてみました。

UIViewのサブクラスとして作り、UITableViewのデリゲートも必要ですから、クラスの大枠は次のようになります。

// 今回作ったクラスの入れ物です
class PhotoSelector: UIView, UITableViewDelegate, UITableViewDataSource {

// ここに具体的なコードが入ります(これ以降で順番に登場します)

}

そうそう、Photosフレームワークを使いますから、忘れずに「import Photos」を先頭に付けてください。

 

大きさが可変ですから、全体のレイアウトを決めなければなりません。ソースコードからレイアウトが見えるように、余白なども含めた固定値と、可変部分の変数を一緒に並べます。横方向と縦方向のそれぞれで、実際の並び順に合わせて変数を定義します。具体的には、次のようなSwiftコードとなりました。

// レイアウト(横方向)
let W_MARGIN_LEFT: CGFloat = 5
let W_NAME_TABLE: CGFloat = 180
let W_MARGIN_CENTER: CGFloat = 10
let W_NAME_TABLE2: CGFloat = 220
let W_MARGIN_CENTER2: CGFloat = 10
var w_image: CGFloat = 0          // 全体Viewサイズと連動して可変
let W_MARGIN_RIGHT: CGFloat = 5
// レイアウト(縦方向)
let H_MARGIN_TOP: CGFloat = 5
let H_LABEL_W_MARGIN: CGFloat = 30
var h_image: CGFloat = 0          // 全体Viewサイズと連動して可変
let H_MARGIN_BOTTUM: CGFloat = 5

見てのとおり、var変数が可変値で、let変数が固定値です。全体のViewサイズと連動することを、コメントで明記しています。

以前にも、同じような考え方でレイアウトを表現しました。今回は、少し改良してあります。以前は固定値だけの並びでしたが、今回は途中に可変値を入れて、可変値の変数名を探しやすくしてあります。

 

続いても、固定値を入れた変数です。

// UIViewの最小サイズ
let VIEW_WIDTH_MIN: CGFloat = 600
let VIEW_HEIGHT_MIN: CGFloat = 350
// 画像の最小サイズ
let IMG_WIDTH_MIN: CGFloat = 10
let IMG_HEIGHT_MIN: CGFloat = 10

// TableViewセルのid
let ID_CELL: String = "psvCell"
let ID_CELL2: String = "psvCell2"

画像の最小サイズの値は、深い意味はありません。小さい画像として生成できるように配慮し、小さな値としました。ですから、妥当な値か検査する意味は持たせられず、マイナスなどの変な値を発見する意味しか持たないでしょう。

 

さらに続いて、データを入れるための変数たちです。

// データ保持
var fetchResult: PHFetchResult!      // アルバム一覧
var fetchResultNum: [Int] = [Int]()  // TableViewから上記一覧を参照するインデックス番号の配列
var fetchAsset: PHFetchResult!       // 選択したアルバムの画像一覧
var phAsset: PHAsset!                // 選択中の画像参照(選択解除後も保持)

// TableView表示用の名前一覧
var aryName: [String] = [String]()   // アルバム名の一覧
var aryName2: [String] = [String]()  // ファイル名の一覧

// 状態
var isSelected: Bool = false         // 画像を選択中かどうか

// デフォルト画像
let defaultImg: UIImage = UIImage(named:"unselected.png")!
// 生成画像のサイズ(デフォルト値付き)
var imgWidth: CGFloat = 1000
var imgHeight: CGFloat = 1000

変数を使うソースコードと一緒に見なければ、役割は分からないと思います。ですから、必要な時点で参照してください。

 

いよいよ、処理するコードに移ります。最初は初期化です。UIView自体には何も手を加えていないので、別な名前で初期化の処理を作っています。

// ======================================== 初期化
func setupF(rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat) {
    if (rWidth < VIEW_WIDTH_MIN) { zSendErrMsgF("ERROR:PHV_STUP:Viewの幅が不足"); return }
    if (rHeight < VIEW_HEIGHT_MIN) { zSendErrMsgF("ERROR:PHV_STUP:Viewの高さが不足"); return }
    self.frame = CGRectMake(rX, rY, rWidth, rHeight)
    // サイズ計算
    w_image = rWidth - (W_MARGIN_LEFT + W_NAME_TABLE + W_MARGIN_CENTER + W_NAME_TABLE2 + W_MARGIN_CENTER2 + W_MARGIN_RIGHT)
    h_image = rHeight - (H_MARGIN_TOP + H_LABEL_W_MARGIN + H_MARGIN_BOTTUM)
    // アルバムの取得
    //let iType: PHAssetCollectionType = PHAssetCollectionType.Album         // 値メモ .Album, .SmartAlbum
    //let iSubtype: PHAssetCollectionSubtype = PHAssetCollectionSubtype.Any  // 値メモ .Any, .AlbumRegular
    fetchResult = PHAssetCollection.fetchAssetCollectionsWithType( .Album, subtype: .Any, options:nil)
    for var i = 0 ; i < fetchResult.count ; i++ {
        let iCollection = fetchResult[i] as? PHAssetCollection
        if (fetchAssetF(iCollection!).count > 0) {    // 画像ファイルが含まれるアルバムだけを配列に追加
            aryName.append(iCollection!.localizedTitle)
            fetchResultNum.append(i)       // 後から参照できるように、インデックス番号を配列に追加
        }
    }
    // TableViewやUI部品の生成
    setupTblViewF()
    setupTblView2F()
    setupUiPartsF()
}
// アルバム内の画像ファイル一覧を取得
private func fetchAssetF(rCollection:PHAssetCollection) -> PHFetchResult {
 let iOptions = PHFetchOptions()
 iOptions.predicate = NSPredicate(format:"mediaType = %d", PHAssetMediaType.Image.rawValue)
 let iFetchAsset: PHFetchResult = PHAsset.fetchAssetsInAssetCollection(rCollection, options:iOptions)
 return iFetchAsset
}

いつものように、要点だけ解説します。サイズ計算では、レイアウトを見せるために並べた変数のうち、可変値の値を計算しています。

今回の肝は、Photosフレームワークの機能でしょう。アルバムの取得では、fetchAssetCollectionsWithTypeメソッドでアルバム一覧を取得してます。その際に、typeとsubtypeを指定します。見てのとおりSwiftでは、短い名前の値で指定できます。

ただし、その値が何なのかは、指定したソースコードだけでは分かりづらいでしょう。作ってから時間が経つと、もう完全に忘れてしまいます。後から思い出しやすいようにと、iTypeとiSubtypeの変数宣言とともに、コメントとして残しました。

でも、これはただのコメントではありません。先頭のスラッシュ2個を削除すると、きちんとしたコードとしてコンパイルされるのです。また、ピリオド以降を削除してから再入力しようとすると、Xcodeの入力補助機能によって、選べる候補の全部がポップアップ表示されます。どんな値が代入可能なのか、その場で調べられるというわけです。こうして調べた値のうち、代表的な値だけを、その左側にあるコメント部分にコピーしてあります。

もう1つ、注意点を。2つの変数宣言とも、データ型を明記しています。代入する値から、データ型を推測できますが、あえてデータ型を入れています。これにより「おそらくデータ型は〜だろう」ではなく、「データ型は確実に〜だ」と理解できるからです。私のスタイルは、データ型を積極的に明記する方針なので、ここでも同様に作りました。

忘れたときに助かる情報をコメント化して残す方法は、今回作っている途中で思い付きました。もっと早く思い付いていれば、今まで作ったライブラリにも同様の情報が残せたのにと感じました。既存ライブラリをメンテする機会などで、積極的に追加しようと思います。

fetchAssetCollectionsWithTypeで得られた結果から、順番に名前を取り出して、TableView用の配列に入れます。その際、アルバムの中身が空の場合も考えられるので、fetchAssetF関数で得られた結果から、アルバム内の要素数を調べて、ゼロならば追加しないようにしました。このように間が抜けると、アルバム一覧配列fetchResultとTableView表示配列aryNameで要素の位置がズレるので、別な配列fetchResultNumにインデックス番号を保存しています。

TableViewやUI部品の生成は、後から紹介する関数を呼び出しているだけです。

アルバム内の画像ファイル一覧を取得するfetchAssetF関数では、オプション設定のPHFetchOptionsインスタンスを生成し、画像ファイルを対象とするように指定しました。この関数が返すデータ型も、アルバム一覧として得たデータ型と同じ、PHFetchResultです。どちらも参照情報を保存するデータですから、標準化して共通で使っているのでしょう。

 

いよいよ画面上のUI部品を作る処理で、前述の初期処理から呼ばれます。まずは、最初のTableVidewから。

// ======================================== アルバム一覧TableView
var tbvName: UITableView!

private func setupTblViewF() {
    // Label
    let iX: CGFloat = W_MARGIN_LEFT + 5
    let iY: CGFloat = H_MARGIN_TOP
    let iWidth: CGFloat = W_NAME_TABLE + 150
    let iHeight: CGFloat = 25
    let lblAList: UILabel = zCreateLblF("フォトアルバムから画像選択", 18, ALIGN_LEFT, iX, iY, iWidth, iHeight)
    self.addSubview(lblAList)
    // TableView
    let iX2: CGFloat = W_MARGIN_LEFT
    let iY2: CGFloat = H_MARGIN_TOP + H_LABEL_W_MARGIN
    let iWidth2: CGFloat = W_NAME_TABLE
    let iHeight2: CGFloat = h_image
    tbvName = zCreateTblViewF(iX2, iY2, iWidth2, iHeight2)
    tbvName.registerClass(UITableViewCell.self, forCellReuseIdentifier:ID_CELL)
    tbvName.delegate = self
    tbvName.dataSource = self
    tbvName.tag = 1
    self.addSubview(tbvName)
}

最初にTableViewのクラス変数が来てますが、ここに入れた理由があります。関係する処理の近くにいれたほうが参照しやすいし、削除するときなどでも、削除し忘れが発生しにくいからです。全体で使うもの以外は、こういう形で入れることが多いです。たまに忘れて、前のほうに入れることもありますけど。

最初のラベルは、この機能の画面上に表示するタイトルのようなものです。ファイル選択のダイアログのような感じで使われると予想し、このような文字列を入れてみました。

続くTableViewは、一般的な使い方でしょう。中身の初期化はデリゲート処理に含まれるため、ここには入っていません。

今回はTableViewを2つ使うので、tagに番号を入れて区別してあります。別な実現方法としては、片方を別なUIViewに入れて区別する方法もありますが、以前に作ったときに作りづらかったので、今はtag方式に落ち着いています。

2つのUI部品とも、表示位置と大きさは、それぞれ4行を使って計算しています。これ以降でも同じですが、この形が一番読みやすいソースになると思っているからです。

 

続いて、2つ目のTableViewの初期化処理です。

// ======================================== ファイル一覧TableView
var tbvName2: UITableView!

private func setupTblView2F() {
    // TableView
    let iX2: CGFloat = W_MARGIN_LEFT + W_NAME_TABLE + W_MARGIN_CENTER
    let iY2: CGFloat = H_MARGIN_TOP + H_LABEL_W_MARGIN
    let iWidth2: CGFloat = W_NAME_TABLE2
    let iHeight2: CGFloat = h_image
    tbvName2 = zCreateTblViewF(iX2, iY2, iWidth2, iHeight2)
    tbvName2.registerClass(UITableViewCell.self, forCellReuseIdentifier:ID_CELL2)
    tbvName2.delegate = self
    tbvName2.dataSource = self
    tbvName2.tag = 2
    self.addSubview(tbvName2)
}

最初のTableViewと設定値が異なるだけなので、説明は不要でしょう。TableViewの変数名も「2」を加えた単純なものにしました。ちょっと手抜きしてますね。

 

次に来るのは、TableViewのデリゲートです。2つのTableViewを、tag値で切り分けて使っています。

// ======================================== TableViewのデリゲート
// Cellの総数を返す
func tableView(rTbvName:UITableView, numberOfRowsInSection rSection:Int) -> Int {
    if rTbvName.tag == 1 {
        return aryName.count
    } else {
        return aryName2.count
    }
}
// Cellに値を設定
func tableView(rTbvName:UITableView, cellForRowAtIndexPath rIndexPath:NSIndexPath) -> UITableViewCell {
    if rTbvName.tag == 1 {
        let iCell = rTbvName.dequeueReusableCellWithIdentifier(ID_CELL, forIndexPath:rIndexPath) as! UITableViewCell
        iCell.textLabel!.text = aryName[rIndexPath.row]
        return iCell
    } else {
        let iCell = rTbvName.dequeueReusableCellWithIdentifier(ID_CELL2, forIndexPath:rIndexPath) as! UITableViewCell
        iCell.textLabel!.text = aryName2[rIndexPath.row]
        return iCell
    }
}
// Cellが選択されたとき
func tableView(rTbvName:UITableView, didSelectRowAtIndexPath rIndexPath:NSIndexPath) {
    if rTbvName.tag == 1 {
        // 画像ファイル一覧をTableView2に表示
        let iInt: Int = fetchResultNum[rIndexPath.row]
        let iCollection = fetchResult[iInt] as? PHAssetCollection
        fetchAsset = fetchAssetF(iCollection!)
        aryName2 = [String]()
        for var i = 0 ; i < fetchAsset.count ; i++ {
            let iAsset = fetchAsset[i] as? PHAsset
            aryName2.append(iAsset!.localIdentifier)
        }
        tbvName2.reloadData()
        // 選択した画像のクリアー
        self.imgView.image = defaultImg
        setMsgF(" ")
        isSelected = false
    } else {
        // 画像の表示
        phAsset = fetchAsset[rIndexPath.row] as? PHAsset
        if let iImage: UIImage = convert2ImgF(phAsset, imgWidth, imgHeight) {
            imgView.image = iImage
            let iWidth: Int = Int(iImage.size.width)
            let iHeight: Int = Int(iImage.size.height)
            setMsgF(String(iWidth) + " x " + String(iHeight))   // 画像サイズの表示
            isSelected = true
        }
    }
}
// Cellの選択が外れたとき
func tableView(rTbvName:UITableView, didDeselectRowAtIndexPath rIndexPath:NSIndexPath) {
}

こちらも一般的な使い方と同じです。説明する必要があるのは、セルが選択されたときの処理でしょう。

最初のTableViewセルの選択(tag==1)では、選択されたセルに関して、画像ファイルの一覧を読み込みます。そのファイル名を、2番目のTableViewに入れます。ところがファイル名に相当するものが見付かりません。仕方がないので、String型の中から見付けたlocalIdentifierを設定しました。名前を配列に入れた後、reloadDataメソッドを呼び出して、2番目のTableViewを更新します。

2番目のTableViewを更新すると、今まで選択していた画像をクリアーする必要があります。表示画像をデフォルト画像に入れ替え、メッセージ欄を空にするなど、必要な処理を済ませます。

2番目のTableViewセルが選択されたとき(else)は、その画像を表示します。PHAssetから画像を生成するconvert2ImgF関数を使って、UIImageを得ます。それをUIImageViewに表示した後、画像サイズを示す文字列を生成して、ラベルへ表示します。

セルの選択が外れたときの処理は空ですが、後から処理を追加したいときに、改めて用意しなくて済むように付けています。今回の場合は永遠に使わなそうなので、外しても構わないのですが。

 

長くなってきたので、続きは次回の投稿ということで。一緒に使用例も書く予定です。

 

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

2015年5月6日水曜日

フォトアルバムから画像取得のライブラリ化を試作(全体設計編)

新しいiPadアプリ開発の企画が持ち上がりつつあります。まだ確定ではないのですが、それに使う機能のライブラリ化を試作してみました。手早く作って、とりあえず動かしただけですが、他に面白そうな話題もないので紹介します。

ライブラリ化を試作したのは、iOSのフォトライブラリに含まれる写真を選んで、iPadアプリに取り込む機能です。iOS8から登場した、Photosフレームワークを使って作ってみました。

 

いつものように、まずは全体像を決めます。フォトライブラリに含まれる写真の中から1枚を選ぶわけですから、数が多い場合は意外に大変です。本当に膨大だと、どの方法でも大変でしょうが、ある程度の絞り込みがでできやすいようにと、まずアルバムを選んでから、写真を選ぶ流れに決めました。2段階の選択とするわけです。

この機能の画面表示の形は、次のようにしました。アルバム名の並んだTableViewが、まず左側に表示されます。その中で1つのアルバムを選択すると、その右隣にある別なTableViewに、ファイル名の一覧が表示されます。その中の1枚を選ぶと、右端にある枠内に実際の画像が表示されるという形です。つまり、3つのブロックが横に並んでいて、左から順番に、アルバム名のTableView、ファイル名のTableView、画像表示枠の役割となります。

これらを含む全体Viewも、画面上のサイズを、ある程度まで指定できるように作ります。とりあえず、サイズが可変する部分は、右端の画像表示のブロックに決めました。

 

使い方としては、色々な形に対応できた方がよいでしょう。モーダルダイアログ風に現れて、選択が完了するまで表示し続けるとか。画面の一部として表示したままになり、必要な時だけ操作して使うとか。どちらでも使えるように、作るときにサイズが指定でき、Viewとして貼り付けられる形にしました。UIViewのサブクラスです。

選択した画像のコピーボタンも付けようと考えたのですが、このViewはアプリから呼ばれる側です。呼ばれる側にボタンを付けると、やり取りが美しくなくなりがちです。そこで、このViewにはボタンを付けず、メソッドで画像を渡す形としました。そのメソッドをアプリ側から呼び出すと、画像情報が得られるというわけです。もし画面上でボタンが必要なら、アプリ側でボタンを用意し、ボタンをタップしたときにメソッドを呼び出す形となります。

 

基本的な動作(画面上で見える動作)は、次のようになります。

呼び出されたときに、左側のTableViewに、アルバム名が入っています。その1つをタップすると、選ばれたアルバム内に含まれる写真の一覧が、中央のTableViewに表示されます。とりあえずの試作なので、ファイル名などのテキストを表示します。その中の1つをタップすると、右側の画像表示領域に、選ばれた画像を表示します。続いて、別な画像をタップすると、タップされた名前の画像を、同じく右側の画像表示領域に表示します。

どれかの画像を表示した状態で、左端にアルバム名をタップすることもあるでしょう。その際には、中央のTableViewが更新されて、新たに選択されたアルバムに含まれるファイル名などが表示されます。当然、写真が選択されてない状態に変わり、右側の画像表示領域もクリアーされます。

ここまでは、ライブラリ側を単独で操作している場合の動きです。選択した画像を登録したりコピーする場合は、アプリ側に必要な機能(ボタンなど)を用意して、それを操作してもらいます。

 

画面上の他の要素としては、この機能を短かく伝えるためのラベルを、画面上の左上に入れます。また、使うかどうか分かりませんが、メッセージ欄としてのラベルを、画面上の右上に配置します。試作段階なので、メッセージ欄には画像サイズでも表示すれば、開発中の動作確認に役立つでしょう。

以上の話を、箇条書きで整理すると、次のようになります。

ライブラリ化の試作:フォトアルバムから画像を取得する機能
・UIViewのサプクラスとして作る
・画面上では、以下の3つのブロックに分かれている
 ・左側:アルバムを並べたTableView
 ・中央:画像ファイルを並べたTableView
 ・右側:選択した画像を表示
・画面上の他の要素
 ・この機能を伝えるラベル:Viewの左上に配置
 ・メッセージ欄:Viewの右上に配置(とりあえず画像サイズなどを表示)
・画面上のサイズは、作るときに指定できる(ある程度の範囲で)
 ・上記の画像表示の部分を可変サイズとする
・アプリへ画像情報を渡す方法:アプリがgetメソッドを呼び出す
・細かな動作
 ・アルバムを切り替えた場合は、画像選択をクリアー
 ・画像が選択されてない状態では、アプリ側へ渡す画像はnilとなる
・実装上の注意点
 ・TableViewなどはデフォルト状態のまま使い、凝らないこと(将来の互換性を重視)

大まかな仕様が決まったので、これに沿ってコーディングすることになります。

 

具体的なSwiftコードを示しながら説明すると長くなるので、ソースコードと解説は、次回の投稿で取り上げます。