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コードを示しながら説明すると長くなるので、ソースコードと解説は、次回の投稿で取り上げます。