Documentsフォルダのファイル閲覧機能をライブラリ化する話の続きです。前回は全体設計まで説明したので、今回は具体的なSwiftコードを紹介します。
今回のものは、UIViewのサブクラスとして作ります。また、UITableViewのデリゲートも必要なので、大枠の役目も持つクラスのSwiftコードは次のようになります。
// 今回作るクラス(今回の入れ物:BV_FileView.swift)
class DocsFileView : UIView, UITableViewDelegate, UITableViewDataSource {
}
入れ物だけですから、簡単ですね。
ちなみに、ファイル名は「BV_FileView.swift」としました。独自ライブラリ用の分類コードとして、今までにない分類「BV」の登場です。本当なら「BVC」なのですが、2文字で統一しているために「BV」と短縮しました。「B」で始まる分類コードなので、アプリ独自のSwiftコードではなく、各アプリに共通のライブラリ用Swiftコードだと、ファイル名だけで判別できます。
続いてクラスの中身です。最初に、クラス内には定義値を入れます。レイアウトに関係する、余白などの値を定義した変数です。
こうした変数の定義では「定義値の意味を上手に伝える」ことが非常に大事です。データの意味や関係が想像しやすいように、定義値の名前や並べ方などで工夫します。今回は、次のように並べてみました。
// レイアウトに関係する定義値
// 幅の値(左からの並び順)
let W_MARGIN_LEFT : CGFloat = 10 // 左余白
let W_TABLE_VIEW : CGFloat = 260 // TableView(固定幅)
let W_MARGIN_CENTER : CGFloat = 10 // 中央余白
// ここにTextAreaが入る(幅は可変)
let W_MARGIN_RIGHT : CGFloat = 10 // 右余白
// その他の幅の値
let W_LABEL_CHAR_COUNT : CGFloat = 100 // 文字数表示ラベル
let W_BUTTON_COPY : CGFloat = 260 // コピーボタン
// 高さの値(上からの並び順)
let H_MARGIN_TOP : CGFloat = 10 // 上余白
let H_LABEL_W_MARGIN : CGFloat = 30 // 見出しラベル
// ここにTableViewまたはTextAreaが入る(高さは可変)
let H_BUTTON_MARGIN : CGFloat = 10 // ボタン余白
let H_BUTTON : CGFloat = 30
let H_MARGIN_BOTTOM : CGFloat = 0 // 下余白
Wで始まるのが幅(Width)、Hで始まるのが高さ(Height)です。それぞれフルスペルにすると変数名が長くなるので、1文字だけにしました。このように同じ言葉が何度も登場する場合は、1文字に短縮したほうが、コード全体としては読みやすくなりますから。
1つ目のグループは、幅の値を左から右へと並べています。定義しない値もコメント行として挿入し、左右方向での全体構成を想像できるように配慮しました。この構成に含まれない幅の値は、2つ目のグループに入れてあります。
3つ目のグループは、高さの値を上から下へと並べています。こちらも上下方向での全体構成を想像できるように並べていて、定義しない値もコメント行として挿入してあります。
幅でも高さでも、コメント行として挿入した部分が可変する値です。それを示すために、コメント行に「可変」という言葉を入れて表しました。UIViewの大きさを変えると、この部分の値が変わるのだと理解しやすくするためです。
以上のように、全体像が想像しやすい形で定義値を作ると、わざわざ紙に書いて整理する必要がなくなります。ソースコードのテキストエディタ上だけで設計するためには、こうした工夫が欠かせません。同時に、余計なドキュメントが不要なソースコードとして仕上がるメリットも生まれます。
続いて、レイアウト関係以外の定義値と、クラスの変数を用意します。次のようになりました。
// クラスの定義値と変数
// TableViewセルのid
let ID_CELL : String = "bvfbCell"
// クラスの重要な値
var viewWidth : CGFloat! // このViewの幅
var viewHeight : CGFloat! // このViewの高さ
var aryFName: [String] = [String]() // ファイル名の一覧
var arySuffix : [String] = ["txt", "plist", "strings", "rtf"] // 内容表示する拡張子の一覧
var enableCopy : Bool = false // コピー可能なファイルならtrue
ファイル内容を表示するかどうかは、拡張子で判断すると決めました。該当する拡張子は、ここで配列に入れてあります。あまり深く考えていなくて、必要になった追加すればよいと思っています。
変数が用意できたので、今度はメソッドです。最初は、初期化から。
サブクラスとして作る場合、親クラスの初期化との関係を考慮して、いろいろと考えるでしょう。しかし、今回のような使い方では、事情が少し違います。親クラスのUIViewをそのまま使っていて、UIView自体には何の拡張も加えていません。UIViewを普通に使い、その上に別なUI部品を追加しているだけです。
このような使い方の場合、サブクラス内では、init()を使った初期化処理は行なわないほうが良いと思います。親クラスの初期化処理をそのまま実行させ、その後でUI部品を追加するという形が、もっとも安全でしょう。もし親クラスで初期化処理などに仕様変更があっても、何の影響も及ぼさないし何の影響も受けないはずですから。
というわけで、init()は作らずに、次のような形で初期化を用意しました。なお、今回の投稿では、私が普段付けている区切り用のコメント行も、ソースコードに含めています。
// ================================================== 初期処理
func setupF(rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat) {
if (rWidth < 800) { println("ERROR:DFV_STUP:Viewの幅が不足"); return }
if (rHeight < 500) { println("ERROR:DFV_STUP:Viewの高さが不足"); return }
viewWidth = rWidth
viewHeight = rHeight
self.frame = CGRectMake(rX, rY, rWidth, rHeight)
// メッセージ欄を作ってから、ファイル名一覧の取得
setupMsgF()
aryFName = zFiles.readFNameListF() // ファイル一覧を取得する
if (aryFName.count < 1) { // ファイルがゼロ個なら終了
zBase.setMsgWTimerF(lblMsg, "対象ファイルがゼロ個なので、この機能は使えません。", .Error, TIME_I_MSG_LL)
return
}
// ファイル名一覧がOKなら、UI部品を生成
setupTblViewF()
setupTxtViewF()
setupBtnCopyF()
}
処理全体を見ると、自分自身(UIView)の表示位置と大きさを指定して、必要なUI部品を生成しています。
一番最初は、自分自身の大きさのチェックです。最低限の大きさより小さい場合は、エラーメッセージを出して、UI部品を何も生成しません。画面上では空のUIViewが表示されるだけで、コンソールへのエラーメッセージでエラーを知ることになります。
続いて、ファイル名の一覧を得るのですが、その前にメッセージ欄だけは生成します。ファイル名の一覧が空なら(ファイルが1つもないなら)、他のUI部品を生成せずにメッセージだけ出して終了します。この場合は、コンソールにではなく、画面上でメッセージを出しています。そうする理由ですが、Documentsフォルダ内が空なのは、使い方のエラーではなく、あり得る状態だからです。その状態で正常なこともあり得るわけですから、ユーザーから見て状況が知れるメッセージを表示する必要があります。
ライブラリの使い方(プログラム上での使い方)のエラーなら、エラーメッセージはコンソールへ、その他のエラーなら画面上のメッセージ欄へ表示が、私の場合の基本です。
ファイルが1つでもあると確認できたら、UI部品を生成する処理(名前がsetupで始まる関数)を順番に呼び出します。それぞれの具体的な処理内容は後述します。
ファイル名の一覧を得るreadFNameListFメソッドは、ファイル関係の共通ライブラリです。ファイル関係の処理を集め、どのアプリにでも使えるように、独自ライブラリとして作ったものです。これ以降でも、ファイル関係の処理は、すべて同じライブラリのメソッドを使っています。具体的な処理内容は難しいものではないので、ここでは掲載しません。そういう処理が別に用意されていると考えて、処理内容を読み取ってください。
続いて、画面上に表示するUI部品の生成処理を1つずつ見ていきます。
その前に、どのUI部品にも共通する書き方を紹介します。それぞれのUI部品の最初には、区切り用のコメント行が付いていて、区切りを明確に表しています。区切りの先頭には、UI部品用の変数が入ります。その後に空白行を1行を入れて、初期化の関数が続きます。このパターンで作ると、UI部品ごとに変数と関数が集まり、ソースコードが見やすく整うからです。
では、最初のUI部品となるTableViewの生成処理を。次のようなSwiftコードになります。
// ================================================== ファイル一覧TableView
var tbvFName: UITableView!
private func setupTblViewF() {
// Label
let iX : CGFloat = W_MARGIN_LEFT + 5
let iY : CGFloat = H_MARGIN_TOP
let iWidth : CGFloat = W_TABLE_VIEW
let iHeight : CGFloat = H_LABEL_W_MARGIN
let lblFileList : UILabel = createLblF("作業場所のファイル一覧", 18, ALIGN_LEFT, iX, iY, iWidth, iHeight)
self.addSubview(lblFileList)
// TableView
let iX2 : CGFloat = W_MARGIN_LEFT
let iY2 : CGFloat = H_MARGIN_TOP + H_LABEL_W_MARGIN
let iWidth2 : CGFloat = W_TABLE_VIEW
let iHeight2 : CGFloat = viewHeight - (H_MARGIN_TOP + H_LABEL_W_MARGIN + H_BUTTON_MARGIN + H_BUTTON + H_MARGIN_BOTTOM)
tbvFName = createTblViewF(iX2, iY2, iWidth2, iHeight2)
tbvFName.registerClass(UITableViewCell.self, forCellReuseIdentifier:ID_CELL)
tbvFName.delegate = self
tbvFName.dataSource = self
self.addSubview(tbvFName)
// 最後のセルまでスクロールさせる
let iRowCount : Int = tbvFName.numberOfRowsInSection(0)
if (iRowCount > 0) {
var pathLastRow : NSIndexPath = NSIndexPath(forRow: iRowCount - 1, inSection:0)
tbvFName.scrollToRowAtIndexPath(pathLastRow, atScrollPosition: .Bottom, animated:true)
}
}
どのUI部品でも、表示位置と大きさを指定します。そのための計算式では、単純に変数を代入するだけだったり、複数の変数を加算や減算して求めたりします。どの計算でも同じ形になるように、4つの変数をletで宣言して、その変数に代入する形式で書きました。このように書き方を統一することは、全体での読み取りやすさに繋がるでしょう。
また、4つの変数を使い回しせず、別な4つの変数をletで宣言する形にしています。もしvarで変数を宣言して、以降で使い回しすると、前側の変数を削除するときに、後ろ側も一緒に直さなければなりません。このような手間を生じさせないために、別々のlet変数を用いています。最近のコンパイラーは最適化が優秀なので、letで統一したほうが良いコードを生成しそうですし。まあ、大した差は生じないのですけど。
TableViewを生成して設定する処理では、特別に難しいことをしていません。独自ライブラリのグローバル関数createTblViewFを使ってTableViewを生成し、必要最低限の設定をしているだけです。
ファイル名一覧を生成した後、最後のセルまでスクロールさせています。ファイル名は日付を含んだりして、もっとも新しいファイルが一番下に来ている可能性が高いのです。表示した状態で、それが見えたたほうが使い勝手が良いので、この処理を加えています。なお、念のためにですが、セルの数がゼロでないかをチェックしています。
TableViewを使う際には、デリゲートに対応したメソッドを用意する必要があります。生成処理に続けて、次のようなSwiftコードを付けました。
// ================================================== TableViewのデリゲート
// Cellが選択されたとき
func tableView(rTbvFName:UITableView, didSelectRowAtIndexPath rIndexPath:NSIndexPath) {
let iFName : String = aryFName[rIndexPath.row]
if checkSuffixF(iFName) {
let iStr : String? = zFiles.readFileDocsF(aryFName[rIndexPath.row])
if iStr == nil {
enableCopy = false
txvFileText.text = "内部エラー:DFV_TV:選択ファイルの読み込みに失敗"
lblFileSize.text = "文字数:不明"
} else {
enableCopy = true
txvFileText.text = iStr
lblFileSize.text = "文字数:" + String(countElements(iStr!))
}
} else {
enableCopy = false
txvFileText.text = "このファイルは、内容表示の対象外です。"
lblFileSize.text = ""
}
}
private func checkSuffixF(rFName:String) -> Bool {
for iSuffix in arySuffix {
if (rFName as NSString).pathExtension == iSuffix { return true }
}
return false
}
// Cellの選択が外れたとき
func tableView(rTbvFName:UITableView, didDeselectRowAtIndexPath rIndexPath:NSIndexPath) {
// 将来の仕様変更のために用意(現在の処理は空)
}
// Cellの総数を返す
func tableView(rTbvFName:UITableView, numberOfRowsInSection rSection:Int) -> Int {
return aryFName.count
}
// Cellに値を設定
func tableView(rTbvFName:UITableView, cellForRowAtIndexPath rIndexPath:NSIndexPath) -> UITableViewCell {
let iCell = rTbvFName.dequeueReusableCellWithIdentifier(ID_CELL, forIndexPath:rIndexPath) as UITableViewCell
iCell.textLabel!.text = aryFName[rIndexPath.row]
return iCell
}
Cellが選択されたときの処理は、選択されたCellからファイル名を得て、登録されている拡張子かどうか調べます。該当する拡張子なら、ファイルを読みにいきます。正常に読めなければエラーメッセージを出し、正常に読めたらTextViewにファイル内容を、Labelに文字数を表示します。また、登録されている拡張子と一致しない場合は、TextViewへのメッセージで知らせます。
ここでは、TextViewやLabelへのアクセスで間接参照を利用せず、値を直接入れています。どちらのUI部品も、ここでしか使わないので、わざわざ間接参照を使いませんでした。このように小さな処理の場合には、間接参照を使わないこともあります。
拡張子を調べる関数checkSuffixFは、NSStringのpathExtensionを使っています。NSStringは機能が充実しているので、SwiftのStringをNSStringにキャストして使うことが多いです。
Cellの選択を外れたときのメソッドは、入れ物だけあって、中身の処理は空のままです。これを付けなくても構わないのですが、あとから仕様変更するときに、わざわざ調べて追加する手間をなくそうと、入れておきました。これを入れても、実害が出るほど遅くならないので入れています。当然ですが、空の状態であることをコメントで表しています。
長くなったので、ここで一旦区切ります。続きは、次の投稿にて。
(使用開発ツール:Xcode 6.1.1, SDK iOS 8.1)