_mitty
@_mitty

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

SwiftUIで配列をDocumentディレクトリに保存する方法

解決したいこと

SwiftUIの勉強で、配列をDocumentディレクトリに保存するコードを書こうとしています。
文字列をDocumentディレクトリに保存して読み込むコードを流用して、配列を保存して読み込むコードを作成しようとしましたが、うまくできません。

具体的には、
String型の部分を一部AnyObject型に変更するなど試しました。
(読みづらくなってしまうため、変更前のコードを載せています。変更しようとした内容は、コメントでコード内に書かせていただきました)

これは筋違いなやり方でしょうか。
配列をDocumentディレクトリに保存するためのコードをご教授いただけないでしょうか。
よろしくお願いします。

***補足***
以下に載せているコードは、大きく分けると、以下の①と②の部分からなっています。
・文字列をDocumentディレクトリに保存して読み込むコード
(以下コードの①の部分と関数)
・配列を保存して読み込むコード
(以下コードの②の部分、グレーアウトにしています)

該当するソースコード

//Xcodeのバージョン Version 12.3 (12C33)
//Swiftのバージョン Apple Swift version 5.3.2

import SwiftUI


var testarray1: [String] = ["a", "b", "c"]
var testarray2: [String] = []

struct ContentView: View {

    @State private var tmpdata1: String = ""
    @State private var tmpdata2: String = ""
    @State private var tmpdata3: String = ""

    var body: some View {

        VStack(alignment: .leading, spacing: 5) {

            //テキストの保存・読み込み・・・・・・・・・①
            //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            //変数tmpdata1に、ユーザーが入力したString型データを代入
            TextField("保存したいキーワードを入力", text: $tmpdata1)
            //保存ボタンが押されたら、「tmpdata1」を「for_textsave.txt」に保存
            Button(action: {
                self.writingToFile(savedata: tmpdata1, savename: "for_textsave.txt")
            }) {
                Text("テキスト保存")
            }
            //テキスト読み出しボタンが押されたら、
            //「for_textsave.txt」から読み出したString型データを変数tmpdata2に代入
            Button(action: {
                tmpdata2 = self.readFromFile(savename: "for_textsave.txt")
            }) {
                Text("テキスト読み出し")
            }
            Text("\(tmpdata2)が読み出されました")
            //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

            Text("")
            /*
            //配列の保存・読み込み(コメントアウトしています)・・・・・・・・・②
            //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            //配列に追加ボタンが押されたら、配列testarray1にtmpdata1を追加
            Button(action: {
                testarray.append(tmpdata1)
                self.writingToFile(savedata: testarray1, savename: "for_arraysave.***")
            }) {
                Text("配列に追加")
            }
            //配列を保存ボタンが押されたら、配列testarray1を「for_arraysave.***」に保存
            Button(action: {
                self.writingToFile(savedata: testarray1, savename: "for_arraysave.***")
            }) {
                Text("配列を保存")
            }
            //配列を読み出しボタンが押されたら、
            //「for_arraysave.***」から読み出したArray型データを変数testarray2に代入
            Button(action: {
                testarray2 = self.readFromFile(savename: "for_arraysave.***")
                tmpdata3 = testarray2.last
            }) {
                Text("配列読み出し")
            }
            Text("\(tmpdata3)が読み出されました")
            //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            */
        }

    }

    // ファイル書き込み===================================================================
    func writingToFile(savedata: String, savename: String) { //←savename:String を savename:AnyObject に変更
        // DocumentsフォルダURL取得
        guard let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("フォルダURL取得エラー")
        }
        // 対象のファイルURL取得
        let fileURL = dirURL.appendingPathComponent(savename)
        // ファイルの書き込み
        do {
            try savedata.write(to: fileURL, atomically: true, encoding: .utf8)
        } catch {
            print("Error: \(error)")
        }
    }
    // =================================================================================

    // ファイル読み込み====================================================================
    func readFromFile(savename: String) -> String { //←savename:String を savename:AnyObject に変更
        // DocumentsフォルダURL取得
        guard let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("フォルダURL取得エラー")
        }
        // 対象のファイルURL取得
        let fileURL = dirURL.appendingPathComponent(savename)
        // ファイルの読み込み
        guard let fileContents = try? String(contentsOf: fileURL) else {
            fatalError("ファイル読み込みエラー")
        }
        /// 読み込んだ内容を戻り値として返す
        return fileContents
    }
    // =================================================================================

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
0

4Answer

最初の回答が適切ではありませんでした。「Codableを使うという手もあるのでよかったら調べてみてください。」と書きましたが、「Codableを使うべき」です。ごめんなさい。

そもそもの話になりますが、.txtファイルに配列は保存できません。配列は文字列ではないからです。どうしても文字列として保存する場合は、例えば配列を\nなどでつなぎ一つの文字列にすることが考えられます。

この場合["a", "b", "c"]を保存すれば

a
b
c

というテキストファイルとなります。

しかしこの方式では改行を含む文字があった場合、つまり["a\nd", "b", "c"]のような場合、保存すると

a
d
b
c

となってしまいます。これを読み込むと["a", "d", "b", "c"]になってしまいます。


Codableはprotocolの一種ですが、これは値を保存可能にするために役立つprotocolです。例えば次の例がCodableを用いている例です。

var strings = ["a", "b", "c"]
let encoder = JSONEncoder()
let data: Data = try! encoder.encode(strings)
let read_strings = try? JSONDecoder().decode([String].self, from: data)
print(read_strings) //Optional(["a", "b", "c"])

3行目に注目してください。dataというのはData型の値です。Data型はバイト列の型ですが、要するに単に0と1の並んだものなので、これをそのままファイルに保存することができます。(もちろんtxtファイルではないので読めるとは限りませんが)。

実際にこれを保存するには

try data.write(to: url, options: .atomicWrite)

のようにします。


ソースコード部分ですが、まずエラーの原因は型が違うことです。

var testarray1: [Data] = ["a", "b", "c"]//エラー1

Cannot convert value of type 'String' to expected element type 'Data'

String型の値を要求されているData型の値に変換できません

これはなぜかというと、DataStringから暗黙に変換できないからです。

ですので、値としては[String]を維持し、保存の関数で受け取った後に上記の方法でtestarray1Dataに変換して、保存すれば良いかと思います。

1Like

Comments

  1. @_mitty

    Questioner

    前回お返事いただいてからだいぶ経ってしましましてすみません。
    まずは、ご回答いただきありがとうございます。
    読み取り・書き換え・保存したいデータを、JSONデータに変換する方法で試行錯誤しました。
    その結果、
    読み取りはできたのですが、書き込みができませんでした。
    プロジェクトを実行する際に、jsonファイルもコンパイルされるため、書き込みは不可であるという記述を見つけて、また行き詰まってしまいました。
    https://teratail.com/questions/163591
    上に載せているソースコード②に、具体的に手を加えていただけないでしょうか。。

配列をAnyObjectではなくData型に変換して、Data.writeを用いてください。おおよそStringと同じ方法で読み書きできますが、Stringとは保存する値の型が違うので、関数は分ける必要があると思います。
Codableを使うという手もあるのでよかったら調べてみてください。

0Like

Comments

  1. @_mitty

    Questioner

    ご回答ありがとうございます。
    いただいたアドバイスを元に書き換えてみましたが、うまくいきません。
    遅々とした進捗で、細かい質問となってしまいすみません。
    配列をData型に変換する方法から理解できていないのですが、以下のコードのどの部分がまずいのでしょうか。
    以下に「ソースコード②」として投稿させていただきました。
    また、アドバイスいただけましたら幸いです。
    よろしくお願いいたします。

###ソースコード②
以下コード内で該当するエラー文があった行に、コメントでエラー1、2、3と書いております。
・エラー1
Cannot convert value of type 'String' to expected element type 'Data'
配列をData型に変換するとは、以下のコードのようにすることとは違うのでしょうか。
・エラー2
Cannot convert value of type 'String' to expected argument type 'Data'
エラー1に関連したエラーだと思いますが、載せさせていただきます。
・エラー3
Cannot convert value of type '[Data]' to expected argument type 'Data'
エラー1に関連したエラーだと思いますが、載せさせていただきます。

import SwiftUI


var testarray1: [Data] = ["a", "b", "c"]//エラー1
var testarray2: [Data] = []

 
struct ContentView: View {

    @State private var tmpdata1: String = ""
    @State private var tmpdata2: String = ""
    
    var body: some View {
        
        VStack(alignment: .leading, spacing: 5) {
            
            //変数tmpdata1に、ユーザーが入力したString型データを代入
            TextField("保存したいキーワードを入力", text: $tmpdata1)
            
            //配列に追加ボタンが押されたら、配列testarray1にtmpdata1を追加
            Button(action: {
                testarray1.append(tmpdata1)//エラー2
            }) {
                Text("配列に追加")
            }
    
            //配列testarray1を「for_arraysave.***」に保存
            Button(action: {
                self.writingToFile_Da(savedata: testarray1, savename: "savearray.dat")//エラー3
            }) {
                Text("配列を保存")
            }
            
            //「for_arraysave.dat」から読み出した配列を変数testarray2に代入
            Button(action: {
                testarray2 = self.readFromFile_Da(savename: "savearray.dat")
                ForEach(0..<3) { i in
                    Text("\(testarray2[i])")
                }
            }) {
                Text("配列読み出し")
            }
            
        }
        
    }

    // ファイル書き込み(Data)=============================================================
    func writingToFile_Da(savedata: Data, savename: String) {
        // DocumentsフォルダURL取得
        guard let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("フォルダURL取得エラー")
        }
        // 対象のファイルURL取得
        let fileURL = dirURL.appendingPathComponent(savename)
        // ファイルの書き込み
        do {
            try savedata.write(to: fileURL)
        } catch {
            print("Error: \(error)")
        }
    }
    // =================================================================================
    
    // ファイル読み込み(Data)=============================================================
    func readFromFile_Da(savename: String) -> Data {
        // DocumentsフォルダURL取得
        guard let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("フォルダURL取得エラー")
        }
        // 対象のファイルURL取得
        let fileURL = dirURL.appendingPathComponent(savename)
        // ファイルの読み込み
        guard let fileContents = try? Data(contentsOf: fileURL) else {
            fatalError("ファイル読み込みエラー")
        }
        /// 読み込んだ内容を戻り値として返す
        return fileContents
    }
    // =================================================================================
    
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
0Like

これでいかがでしょうか。

struct ContentView: View {
    //グローバルな変数をView内部に移動
    @State private var testarray1: [String] = ["a", "b", "c"]
    @State private var testarray2: [String] = []

    @State private var tmpdata1: String = ""
    @State private var tmpdata2: String = ""

    var body: some View {

        VStack(alignment: .leading, spacing: 5) {

            //変数tmpdata1に、ユーザーが入力したString型データを代入
            TextField("保存したいキーワードを入力", text: $tmpdata1)

            //配列に追加ボタンが押されたら、配列testarray1にtmpdata1を追加
            Button(action: {
                testarray1.append(tmpdata1)
            }) {
                Text("配列に追加")
            }

            //配列testarray1を「for_arraysave.***」に保存
            Button(action: {
                self.writingToFile_Da(savedata: testarray1, savename: "savearray.dat")
            }) {
                Text("配列を保存")
            }

            //「for_arraysave.dat」から読み出した配列を変数testarray2に代入
            Button(action: {
                testarray2 = self.readFromFile_Da(savename: "savearray.dat")
            }) {
                Text("配列読み出し")
                //ForEachをlabelの位置に移動
                ForEach(testarray2.indices, id: \.self) { i in
                    Text("\(testarray2[i])")
                }
            }

        }

    }

    // ファイル書き込み(Data)=============================================================
    func writingToFile_Da(savedata: [String], savename: String) {
        // DocumentsフォルダURL取得
        guard let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("フォルダURL取得エラー")
        }
        // 対象のファイルURL取得
        let fileURL = dirURL.appendingPathComponent(savename)
        // ファイルの書き込み//JSONEncoderを利用
        do {
            let encoder = JSONEncoder()
            let data: Data = try encoder.encode(savedata)
            try data.write(to: fileURL)
        } catch {
            print("Error: \(error)")
        }
    }
    // =================================================================================

    // ファイル読み込み(Data)=============================================================
    func readFromFile_Da(savename: String) -> [String] { //[String]を返す仕様に変更
        // DocumentsフォルダURL取得
        guard let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("フォルダURL取得エラー")
        }
        // 対象のファイルURL取得
        let fileURL = dirURL.appendingPathComponent(savename)
        // ファイルの読み込み//JSONDecoderを利用
        do{
            let fileContents = try Data(contentsOf: fileURL)
            let read_strings = try JSONDecoder().decode([String].self, from: fileContents)
            /// 読み込んだ内容を戻り値として返す
            return read_strings
        }catch{
            fatalError("ファイル読み込みエラー")

        }
    }
    // =================================================================================

}
0Like

Comments

  1. @_mitty

    Questioner

    ありがとうございます!
    実際に動くコードにしてくださり、大変助かりました。
    長いこと行き詰っておりましたが、ようやく先へ進めそうです。
    ご回答、ありがとうございました。

Your answer might help someone💌