iOS開発メモ5 (MediaUploaderの開発1)

前回までで、ViewControllerなどのソースファイルを作成し、StoryBoardとの紐付けまで完了した。今回はUI部品の紐付けとCollectionViewで、写真ロールの画像をタイル表示するところまでを行う。

UI部品の紐付け

CollectionViewのアウトレットをFirstViewControllerに定義して、このコントローラーから操作できるようにする。CollectionView上で右クリックメニューを表示し、New Referencing Outletからドラッグして、紐付けする。

ss--2017-10-18-17.24.37

設定は以下とする。名称はcollectionViewにしておく。

ss--2017-10-23-16.49.39

ss--2017-10-23-16.49.59

SecondViewControllerとTableViewについても同様にアウトレットを作成しておく。

ss--2017-10-23-16.51.22

設定は以下とする。名称はtableViewにしておく。

ss--2017-10-23-16.51.31

CollectionViewのセル上に配置したImageViewのアウトレットは、親Cellが持っているbackgroundViewとして紐付ける。Cellには標準で幾つかの役割を持つビューがあり、そのひとつのビューとして割り当てる。(詳細は別途記述)

ss--2017-10-23-17.06.39

ss--2017-10-23-17.06.48

ss--2017-10-23-17.06.58

TableViewセルに配置した部品に関しては、Tagにユニークな番号を発番しておく。セルの生成時にこれらの部品を番号で指定できるようにしておく。

ss--2017-10-23-17.21.45

ss--2017-10-23-17.23.38

ss--2017-10-23-17.24.08

StoryBoard上に配置した部品へアクセスする方法としては、アウトレットを定義する方法とタグ番号を指定して部品を取り出す方法など幾つかある。どれを使っても構わない。

ss--2017-10-23-17.24.16

CollectionViewのデータソース、デリゲートの実装

CollectionViewのさまざまな動作は幾つかのデータソースやデリゲートなどのプロトコルで定義されている。

プロトコル 役割
UICollectionViewDataSource データプロバイダ
UICollectionViewDelegate セルの選択、ハイライトなどの制御
UICollectionViewDelegateFlowLayout セルのレイアウト配置

必要に応じて、上記のプロトコルを実装することになるが、最低でも上記3つのプロトコルにある必須メソッドについては実装しなければならない。

また、ViewController上で実装したメソッドを呼び出してもらうため、上記のプロトコルとコントローラを紐付ける。

ss--2017-10-23-17.59.51

ss--2017-10-23-18.00.04

これは以下のコーディングを実装しても同じ意味になる。

class FirstViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        collectionView.dataSource = self
        collectionView.delegate = self
    }
}

ss--2017-10-23-18.00.15

tableViewについても同様にSecondViewControllerと紐づけておく。

ss--2017-10-23-18.00.40

コレクションビューのセルに写真を表示する

UICollectionViewDataSourceを実装してとりあえずセルに写真を表示するところまで実装する。

まず、iOS10?からアプリケーション構成ファイルに、端末のどのデータにアクセスするかを宣言しておく必要がある。宣言がないと、アプリ起動中にエラーになり起動しない

以下は、記載せずにデバッグ起動したときのエラーが発生したときのメッセージ

2017-10-24 09:25:35.857482+0900 MediaUploader[9576:169233] [access] This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSPhotoLibraryUsageDescription key with a string value explaining to the user how the app uses this data.
(lldb) 

Info.plistファイルに以下のノードを追加して、写真ロールにアクセスすることを宣言する。

<plist version="1.0">
<dict>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>(アクセスする理由など記述する)</string>
    :
</dict>
</plist>

ちなみに、構成ファイルに追加した後、再度起動すると、以下のようなメッセージが表示される。
赤枠で囲った箇所に上記のstringノードのテキストが表示される。

ss--2017-10-24-2.32.43

次に、写真ロールからアセット情報を取得処理を追加する。今回はアセット情報を参照するだけで、アプリの起動中にアセット情報が増えたり減ったりしないので、viewDidLoadメソッド内で1度だけ、アセット情報を取得してメンバに保持する。

import UIKit
import Photos

class FirstViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!
    
    let imageManager = PHImageManager()
    var fetchResult: PHFetchResult<PHAsset>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // fetch asset data
        let opt = PHFetchOptions()
        opt.sortDescriptors = [
            NSSortDescriptor(key: "creationDate", ascending: false)
        ]
        fetchResult = PHAsset.fetchAssets(
            with: PHAssetMediaType.image, options: opt)
        // reload collection view
        collectionView!.reloadData()
    }

UICollectionViewDataSourceについて、今回はextensionとして実装してみる。このプロトコルには幾つかのメソッドがあるが、numberOfItemsInSectioncellForItemAtが最低限実装が必要なメソッドになる。

CollectionViewのreloadDataをコールすると、numberOfItemsInSectionがシステム側からコールされるので、セクションごとのセル数を返す必要がある。セクションが1つしかない場合は、このメソッドは1回だけコールされる。

今回は1つしかないセクションに取得した写真を全て表示するので、アセット情報の個数を返す。

extension FirstViewController : UICollectionViewDataSource {
    // numberOfItemsInSection
    func collectionView(_ collectionView: UICollectionView,
        numberOfItemsInSection section: Int) -> Int {
        var cnt = 0;
        if( fetchResult != nil ){
            cnt = fetchResult.count
        }
        print("collection count ="+String(cnt));
        return cnt
    }

セル数が0より多い数を指定すると、次に描画するセルごとにcellForItemAtがコールされる。セルのインデックスが入力引数として渡される。このメソッドは画面上に表示するセルの数だけ表示される。なのでセルのサイズを大きく指定すると、表示できる個数が減るので、コールされる回数も減る。

どのセルを描画する必要があるかは、システム側で計算されてcellForItemAtがコールされるので、開発側は入力されたインデックス番目に該当するアセット情報からイメージを取得して、セルに貼り付けるだけで良い。

extension FirstViewController : UICollectionViewDataSource {
    :
    // cellForItemAt
    func collectionView(_ collectionView: UICollectionView,
        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        // Dequeue a CollectionViewCell
        guard let cell = collectionView.dequeueReusableCell(
            withReuseIdentifier: "Cell", for: indexPath) as?
            CollectionViewCell else{
                fatalError("failed to dequeue for cell")
        }
        
        let asset = fetchResult.object(at: indexPath.item)
        // セルが再利用されるのと、requestImageの結果が非同期で取得される為、
        // 取得後、同じセルにイメージが設定できるように識別子を設定しておく
        // (これをしないと、上書きされてしまう?)
        cell.assetIdentifier = asset.localIdentifier
        
        // thumbnail size
        let thumbnailSize: CGSize = cell.frame.size
        imageManager.requestImage( for: asset, targetSize: thumbnailSize,
            contentMode: .aspectFill, options: nil, resultHandler: {
            (image, info) in
            if cell.assetIdentifier == asset.localIdentifier {
                let bkImageView : UIImageView
                bkImageView = cell.backgroundView as! UIImageView
                bkImageView.image = image
            }
        })
        
        return cell
    }

dequeueReusableCellはCollectionView上で定義したセルを識別子で取得している。また、メソッド名からも連想できるが、セルは再利用される。

仮にCollectionView上に写真を100枚表示するとしても、実際に画面で同時に表示できるセルの数は大きさにもよるが最大15個程度である。このメソッドは、画面外にスクロールアウトして表示されなくなったセルをそのまま再利用して、効率よくリソースを使うことができるので、セルを100個作成しなくて済むような機構になっている。

取り出したセルからサイズを取得し、そのサイズを指定してrequestImageメソッドで画像を作成する。取得した画像(UIImage)をbackgroundViewに設定したUIImageViewにセットすることによって、セルにイメージを描画することできる。

ここまで実装したら、デバッグ実行する。初回のみ写真ロールにアクセスしても良いか確認するパネルが表示されるので、”OK"を選択すると、次回起動時にシミュレータに保存されている画像がセルに表示されるはず。

ss--2017-10-24-2.34.20

インタフェースビルダでサイズ指定したままのセルが表示されるが、2列のみの表示と真ん中に隙間が広がっており、見た目がよろしくないが、とりあえず画像が表示される最低限の実装は完了。次回は画面サイズからセル数を算出して、標準アプリのように綺麗に配列させる方法を記載する予定。