[iOSアプリ開発] ファイルを抽象化した構造体を作る(3)

5 min read読了の目安(約4500字

こんにちは。
ZennではiOSアプリ開発の普段自分が使っているちょっとしたTipsなどを書いていければと思っております。

今回はSwiftでは少し冗長になりがちなファイルの操作を簡単にするために "ファイルを抽象化した構造体" を作っていきます。
当記事だけで終わりというわけではなく、続き物にしていく予定なのでよろしくおねがいします。

前回までこちら

https://zenn.dev/nkysyuichi/articles/467ddcc1041d4e
https://zenn.dev/nkysyuichi/articles/7269df712386ed

ファイルを取り扱う構造体

URLとデータを取得できるようにする

ファイルの操作はパス文字列を使うパターンの他に、ファイルURLで扱うパターンや、データに変換して扱うパターンがあります。主に書き込みなどを行うときですね。
その時のために容易に取得できるようにしておきましょう。

extension File {
    
    var url: URL {
        return URL(fileURLWithPath: path)
    }
    
    var data: Data? {
        return try? Data(contentsOf: url)
    }
}

テキストや画像を取得できるようにする

データが取得できるようになると、そこから実際のファイルの内容を取得することができるはずです。

テキスト

テキストファイルからテキスト内容を取得するのはこんな感じです。
(※textというメソッド名でもいいかも)

extension File {
    
    func contents(encoding: String.Encoding) -> String? {
        guard let data = self.data else { return nil }
        return String(data: data, encoding: encoding)
    }
    
    var contents: String? {
        return contents(encoding: .utf8)
    }
}

エンコーディングはUTF8を使うことも多いでしょうから「文字列エンコーディングを指定できるプロパティ」と「UTF8固定の計算プロパティ」の2種類を用意しています。

画像

画像ファイルはUIImageとして簡単に取り出せるようにしておきます。

// UIImageを扱うのでimportをFoundationからUIKitに変えます
import UIKit

extension File {
    
    var image: UIImage? {
        guard let data = self.data else { return nil }
        return UIImage(data: data)
    }
}

使い方

ファイルの内容を取得するにはこんな感じ

// テキスト
let textFile = File.documentDirectory + "text.txt"
let contents = textFile.contents!

// 画像
let imageFile = File.documentDirectory + "image.png"
let image = imageFile.image!

シンプルに取得できるようになったと思います。

万が一に画像ファイルでないファイルを指定したりするとnilが返るようにしていますので、安全に使うことができるかなと思います。

テキストや画像を書き込めるようにする

逆にファイルに書き込めるようにもしていきましょう。
前回の記事で作った処理がここで生きてくることになります。

テキスト

extension File {

    func write(contents: String, encoding: String.Encoding = .utf8) throws {
        try parentDirectory.makeDirectory()
        try contents.write(to: url, atomically: false, encoding: encoding)
    }
}

読み込むときと同じようにエンコーディングはUTF8をデフォルトにしておき、必要に応じて変えられるようにしておきます。

また、ファイルの親ディレクトリが存在していない可能性もあるので、書き込む前に作っておくことも忘れないようにしておきましょう。

実際に書き込む際には String型の機能を使うことになります。

https://developer.apple.com/documentation/foundation/nsstring/1407654-write

第2引数のatomicallyは、アトミックかどうかの指定になります。
「アトミックかどうか」は不可分操作(原子操作)とも言われますが、端的に言うとファイルが途中で別プロセスに干渉されないようにするトランザクションの話になると思います。

https://ja.wikipedia.org/wiki/不可分操作

ドキュメントによると、atomicallytrueにすると、ファイルの書き込みは一時ファイルで行われ、終わったら元のファイルに置換するという挙動をするとのことです。
その場合、できあがったファイルの作成日時の情報はその実行された時間というふうに書き換わる挙動をします。

ここは作るアプリの性質にもよると思いますが、アトミック処理はたとえば同時多発的なプロセスからファイルアクセスが行われたときの整合性担保のための仕組みだと思いますし、個人の端末で個人だけが使うアプリにおいてはfalse指定でもあまり影響がないかなとも思います。この辺りは要件に合わせて使い分けてください。

使い方

let file = File.documentDirectory + "hoge" + "fuga" + "test.txt"
try? file.write(contents: "Hello World")

これで、ドキュメントディレクトリの下にhoge/fuga/というディレクトリが作られ、"Hello World"と書かれたテキストファイルが作成されると思います。

画像

画像の書き込みはJPEGPNGで書き込めるようにします。(他のフォーマットについては長くなると思うので別の機会があれば)

extension File {

    // JPEGで書き込む
    func write(imageAsJpeg image: UIImage, quality: CGFloat = 0.9) throws {
        guard let data = image.jpegData(compressionQuality: quality) else { return }
        try parentDirectory.makeDirectory()
        try data.write(to: url)
    }
    
    // PNGで書き込む
    func write(imageAsPng image: UIImage) throws {
        guard let data = image.pngData() else { return }
        try parentDirectory.makeDirectory()
        try data.write(to: url)
    }
}

テキストファイルと同様に先にディレクトリを作っておくことがミソです。

UIImageには画像をData型に各フォーマット用に変換する機能があるので素直にそれを使って書き出します。

JPEGについては圧縮率の指定が必要ですが、下記の記事を参考にして0.9がデフォルト値として適当かなと思いました。

https://grandbig.github.io/blog/2020/05/30/jpeg-compression/

※ちなみに、jpegData()image.pngData()からnilが返ってくるときも例外を吐いたほうがメンテナンス性は高まるかもしれません。今回はスキップしています。

使い方

// JPEG
let image = UIImage(named: "sample.jpg")!
let file = File.documentDirectory + "hoge" + "fuga" + "test.jpg"
try? file.write(imageAsJpeg: image)

// PNG
let image = UIImage(named: "sample.png")!
let file = File.documentDirectory + "hoge" + "fuga" + "test.png"
try? file.write(imageAsPng: image)

ゴリゴリと画像の書き込み処理を書くよりシンプルになったと思います。

まとめ

ここまでで

  • ファイルURLを簡単に取得できるようにした
  • ファイルの中身をData型で簡単に取得できるようにした
  • テキストファイルの内容を取得できるようにした
  • 画像ファイルの画像をUIImageで取得できるようにした
  • テキストファイルに書き込めるようにした
  • 画像ファイルにUIImageの内容を書き出しできるようにした

というところまでやりました。

ずいぶんとここまでで便利になってきたと思います。
次回以降でまだまだ便利していきましょう。

ではまた。