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)

0 件のコメント:

コメントを投稿