SwiftUIでテキストファイルを String(contentsOf: ) で読み込む場合の表示が遅い

アプリ内に内包したテキストファイル(例えば利用規約など)を読み込んで表示させるまでに時間がかかる。15K bytesくらいのファイル。遅いといっても3秒〜5秒くらいなんですが、実際操作した時に表示させようとボタンなりを押して、たかだかテキストの表示で3秒〜5秒遅いというのは結構致命的ということで調査することに。

元々遅かったコードが以下。

struct EULAView: View {
    @State var eula: String = ""

    var body: some View {
        VStack {
            Text("利用規約").font(.headline).foregroundColor(.black)
            ScrollView(content: {
                VStack {
                    Text("\(self.eula)").multilineTextAlignment(.leading).foregroundColor(.black)
                }
            })
        }
        .onAppear(perform: {
            Task.detached { @MainActor in
                guard let fileUrl = Bundle.main.url(forResource: "eula_ja", withExtension: "txt") else {
                    fatalError("Not found license file.")
                }
                let eula = try String(contentsOf: fileUrl)
            }
        })
    }
}

いろいろ調べてみると String(contentsOf: ) が遅いらしい。

調べて行き着いた方法は String ではなく Data を使うことに。ついでに VStack を LazyVStack に変更。

struct EULAView: View {
    @State var eula: [Data] = []

    var body: some View {
        VStack {
            Text("利用規約").font(.headline).foregroundColor(.black)
            ScrollView(content: {
                LazyVStack {
                    ForEach(0..<eula.count, id: \.self) { line in
                        if let text = String(data: eula[line], encoding: .utf8) {
                            Text("\(text)\n")
                                .multilineTextAlignment(.leading)
                                .foregroundColor(.black)
                                .frame(maxWidth: .infinity, alignment: .leading)
                        }
                    }
                }
            })
        }
        .onAppear(perform: {
            Task.detached { @MainActor in
                guard let fileUrl = Bundle.main.url(forResource: "eula_ja", withExtension: "txt") else {
                    fatalError("Not found license file.")
                }
                let fileData = try Data(contentsOf: fileUrl)
                eula = fileData.split(separator: Data("\n".utf8))
            }
        })
    }
}

上記のファイルから読み込んだ Data に対するデリミタ(上記では “\n” )による分割(Arrayに)するための Data.split は iOS16 以降から使えるようです。iOS15 以下のサポートも必要な場合は Data を Extension で拡張させて実装する感じで。
以下を見る限り iOS16 以降と言うわけでもなさそうに見えるけどXcode上だとビルド時に怒られる・・・。
split(separator:maxSplits:omittingEmptySubsequences:) (Apple公式)

extension Data {
    func split(separator: Data, omittingEmptySubsequences: Bool = true) -> [Data] {
      var current = startIndex
      var chunks = [Data]()

      while let range = self[current...].range(of: separator) {
          if !omittingEmptySubsequences {
              chunks.append(self[current..<range.lowerBound])
          } else if range.lowerBound > current {
              chunks.append(self[current..<range.lowerBound])
          }
          current = range.upperBound
      }
      if current < self.endIndex {
          chunks.append(self[current...])
      }
      return chunks
  }
}

上記の拡張部分は以下のサイトのものを利用させてもらいました。ありがとうございます。

参考: Split Data by other Data

フォローする