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)
初めまして、katohと申します。色々参考にさせて貰ってます。
返信削除テキストとして読み込む処理が含まれてないので教えてください。
plistファイルはテキスト形式で無い物もあります。
そのようなファイルはどうやって除外してるのですか?
宜しくお願いします。
katohさん、コメントありがとうございます。
削除ご質問の件ですが、手短に回答します。
テキスト形式ではないplistファイルも、テキスト化して表示しています。
つまり、テキスト形式のplistなら、そのまま表示して、
テキスト形式でないplistなら、テキストに変換して表示しているわけです。
その判別処理も含みますので、この欄では書ききれません。
近いうちにブログの投稿で解説します。
書き終わるまで、何日か(おそらく数日ほど)お待ちください。