🦅

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

2021/03/01に公開

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

過去8回にわたって少しずつSwiftでは少し冗長になりがちなファイルの操作を簡単にするために "ファイルを抽象化した構造体" を作ってきました。
今記事ではそのソースコード全体を載せたいと思います。

コピペしていただいても動作すると思います。
このメソッドやプロパティは何だったっけという場合は過去の記事を参照してください。

ソースコード

Xcode:12.4 Swift5

import UIKit

// MARK: ファイルを抽象化した構造体を作る(1)

struct File {
    let path: String
}

extension File {
    
    static let documentDirectoryPath = NSSearchPathForDirectoriesInDomains(
        .documentDirectory,
        .userDomainMask,
        true
    ).first!
    
    static let libraryDirectoryPath = NSSearchPathForDirectoriesInDomains(
        .libraryDirectory,
        .userDomainMask,
        true
    ).first!
    
    static let temporaryDirectoryPath = NSTemporaryDirectory()
    
    static let mainBundlePath = Bundle.main.bundlePath
    
    static let documentDirectory = File(path: documentDirectoryPath)
    static let libraryDirectory = File(path: libraryDirectoryPath)
    static let temporaryDirectory = File(path: temporaryDirectoryPath)
    static let mainBundle = File(path: mainBundlePath)
}

extension File {
    
    func append(pathComponent: String) -> File {
        return File(path: (path as NSString).appendingPathComponent(pathComponent))
    }
    
    static func + (lhs: File, rhs: String) -> File {
        return lhs.append(pathComponent: rhs)
    }
}

extension File: Equatable {
    
    static func == (lhs: File, rhs: File) -> Bool {
        return lhs.path == rhs.path
    }
}

// MARK: ファイルを抽象化した構造体を作る(2)

extension File {
    
    var exists: Bool {
        return FileManager.default.fileExists(atPath: path)
    }
}

extension File {
    
    var parentDirectoryPath: String {
        if path == "/" { return "" }
        return (path as NSString).deletingLastPathComponent
    }
    
    var parentDirectory: File {
        return File(path: parentDirectoryPath)
    }
}

extension File {
    
    func makeDirectory() throws {
        if !exists {
            try FileManager.default.createDirectory(
                atPath: path,
                withIntermediateDirectories: true,
                attributes: nil
            )
        }
    }
}

// MARK: ファイルを抽象化した構造体を作る(3)

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

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)
    }

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

extension File {
    
    var image: UIImage? {
        guard let data = self.data else { return nil }
        return UIImage(data: data)
    }
    
    // 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)
    }
}

// MARK: ファイルを抽象化した構造体を作る(4)

extension File {
    
    var name: String {
        return (path as NSString).lastPathComponent
    }
    
    var `extension`: String {
        let ext = (name as NSString).pathExtension
        return ext.isEmpty ? "" : ".\(ext)"
    }
    
    var extensionWithoutDot: String {
        let ext = (name as NSString).pathExtension
        return ext.isEmpty ? "" : "\(ext)"
    }
    
    var nameWithoutExtension: String {
        return (name as NSString).deletingPathExtension
    }
}

extension File {
    
    var isFile: Bool {
        var isDirectory: ObjCBool = false
        if FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) {
            return !isDirectory.boolValue
        }
        return false
    }
    
    var isDirectory: Bool {
        var isDirectory: ObjCBool = false
        if FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) {
            return isDirectory.boolValue
        }
        return false
    }

// こちらでも可
//  var isDirectory: Bool {
//      return exists && !isFile
//  }
}

// MARK: ファイルを抽象化した構造体を作る(5)

extension File {

    // 作成日時
    var creationDate: Date? {
        return attributes[.creationDate] as? Date
    }
    
    // 更新日時
    var modificationDate: Date? {
        return attributes[.modificationDate] as? Date
    }
    
    // ファイルサイズ
    var size: UInt64 {
        return attributes[.size] as? UInt64 ?? 0
    }
    
    private var attributes: [FileAttributeKey : Any] {
        let attr1 = (try? FileManager.default.attributesOfFileSystem(forPath: path)) ?? [:]
        let attr2 = (try? FileManager.default.attributesOfItem(atPath: path)) ?? [:]
        return [attr1, attr2].reduce(into: [FileAttributeKey : Any](), { ret, attr in
            ret.merge(attr) { $1 }
        })
    }
}

// MARK: ファイルを抽象化した構造体を作る(6)

extension File {

    var files: [File] {
        return filesMap { self + $0 }
    }
    
    var filePaths: [String] {
        return filesMap { (self + $0).path }
    }
    
    var fileNames: [String] {
        return filesMap { $0 }
    }
    
    private func filesMap<T>(_ transform: (String) throws -> (T)) rethrows -> [T] {
        guard let fileNames = try? FileManager.default.contentsOfDirectory(atPath: path) else {
            return []
        }
        return try fileNames.map { try transform($0) }
	// ファイル名で並べたいときは下を使用する
	// return fileNames.sorted().map { try transform($0) }
    }
}

extension File: CustomStringConvertible {
    
    var description: String {
        let type = isDirectory ? "Dir" : "File"
        return "<\(type) \(name)>"
    }
}

// MARK: ファイルを抽象化した構造体を作る(7)

extension File {

    func delete() throws {
        try FileManager.default.removeItem(atPath: path)
    }
    
    func deleteAllChildren() throws {
        try files.forEach { file in
            try file.delete()
        }
    }
    
    func copy(to destination: File, force: Bool = true) throws {
        if force && destination.exists {
            try destination.delete()
        }
        try FileManager.default.copyItem(atPath: path, toPath: destination.path)
    }
    
     func move(to destination: File, force: Bool = true) throws {
        if force && destination.exists {
            try destination.delete()
        }
        try FileManager.default.moveItem(atPath: path, toPath: destination.path)
    }
    
    func rename(to name: String, force: Bool = true) throws -> File {
        let destination = File(path: parentDirectoryPath) + name
        try move(to: destination, force: force)
        return destination
    }
}

// MARK: ファイルを抽象化した構造体を作る(8)

extension File {
    
    func jsonDecoded<T>(_ type: T.Type) throws -> T? where T : Decodable {
        guard let data = self.data else { return nil }
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return try decoder.decode(type, from: data)
    }
}

extension File {
    
    func jsonEncode<T>(_ value: T) throws -> Data where T : Encodable {
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        encoder.keyEncodingStrategy = .convertToSnakeCase
        return try encoder.encode(value)
    }
    
    func writeEncodedJson<T>(_ value: T, encoding: String.Encoding = .utf8) throws where T : Encodable {
        let encoded = try jsonEncode(value)
        let jsonString = String(data: encoded, encoding: encoding) ?? ""
        try parentDirectory.makeDirectory()
        try jsonString.write(to: url, atomically: false, encoding: encoding)
    }
}

extensionで細かく分かれていますが、記事のためなので適宜うまくやってください。

まとめのまとめ

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

  • File構造体を作る
  • 特定のディレクトリをすぐ参照できる
  • パス文字列を+演算子で連結できる
  • Equatableに準拠する

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

  • ファイル(またはディレクトリ)の存在確認ができるようになった
  • 現在のパスの親ディレクトリを取得できるようになった
  • 実際にディレクトリを作る機能をつけた

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

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

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

  • ファイル名と拡張子、またはその組み合わせを取得できるようにしました。
  • そのFile構造体が「実在するファイルなのか」を取得できるようにしました。
  • そのFile構造体が「実在するディレクトリなのか」を取得できるようにしました。

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

  • ファイルの属性値(メタデータ)を取得できるようにする。
  • 属性値取得は隠蔽して、作成日時・更新日時・サイズを取得できるようする。

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

  • ディレクトリ内のファイル(またはディレクトリ)をすべて取得できるようにした
  • 高階メソッドにより実装がシンプルにでき、ループ回数も節約できる
  • すべて取得したときのデバッグの見やすさのためにCustomStringConvertibleに準拠した

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

  • ファイルの削除、コピー、移動をできるようにした
  • 移動を応用することでリネームをできるようにした

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

  • 構造化されたデータはCodableを使用して扱う
  • CodableなデータをJSONファイルを介して取得・保存できるようにした
  • データ生成とファイル処理の責務は切り分けた

最後に

今回作成したFile構造体は自分が使っていて便利だったのでご紹介したものです。
もちろんそれ以外の機能を付けてもいいと思いますし、使用しないものは削除していいと思います。
「いいかも」と思っていただいた方は取り入れてみてくださいませ。

では、また。

Discussion