前節ではSwift内のデータの編集を紹介しました. この節では, これまでSwiftに直接書き込んでいたリストのデータをファイルから読み込んでみます。多くのアプリケーションは用いるデータを外部から得てくることがほとんどです. 今回はその1例としてJSONファイルを読み込んでリスト表示にしてみます.

  • SFSwiftUI03-2.swiftpm を開く (サンプルコード)

    • 03-1を完了したコードと同様
      • photos配列を空っぽにしている
      • 後で読み込むためのJSONファイルが入っている
    • データがないため, プレビューも何もない状態から始まることを確認

コード1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import SwiftUI

struct ContentView: View {
    @State var photos:[PhotoData] = []
    
    var body: some View {
        List {
            Section {
                ForEach( $photos ) { bind_photo in
                    NavigationLink( destination:{
                        SceneryDetail( data:bind_photo )
                    },
                    label: {
                        SceneryRow( data:bind_photo.wrappedValue )
                    })
                }
                .onDelete { index_set in
                    photos.remove( atOffsets:index_set )
                }
            }
            header: {
                HStack {
                    Text( "景色写真" )
                    
                    Spacer()
                    
                    Image( systemName:"plus" )
                    .font( .title2 )
                    .foregroundStyle( Color.accentColor )
                }
            }
        }
        .listStyle( .grouped )
        .navigationTitle( "写真集" )
    
    }
}

初期状態

swiftui_3_2_1_1.png

photos配列に何も入っていないのでリストには何も表示されません. photosにデータが必要になりますが,今回はSwiftファイルに直接データを書かず, PhotoData型に必要なデータを満たしているJSONファイルを読み込んでphotosに入れるようにしてみます.

ファイルのバンドル

今回読み込むJSONファイルはサンプルコードにすでに用意されており,App内に含んでいます. このようにファイルをApp内にあらかじめ含ませておくことを バンドル といいます. 今回はResourcesフォルダとその中身のphotos.jsonがバンドルされています.

swiftui_3_2_1_2.png

JSONとは

JSON(JavaScript Object Notation)はWebでデータを取り扱うのにメジャーなファイル形式です. 「キー:値」 のペアと, その配列でデータを作るテキストファイルを指します.

  • Wikipedia

  • [ ~ ] は配列, { ~ } はペア値のグループ を指します. カンマで区切りながら,下記のように記述します.

コード2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[
    {
        "id": "0",
        "type": "景色",
        "imageName": "CC0_01",
        "title": "景色1",
        "message": "森と山です"
    },
    {
        "id": "1",
        "type": "景色",
        "imageName": "CC0_04",
        "title": "景色2",
        "message": "きれいな湖です"
    }
]

Resources/photos.jsonは, []配列の中に{}でまとめた2つのデータを持ってます. また{}の中のキーペアの値をみると,PhotoData構造体と同じ情報を持っていることがわかります.これからこのファイルを読み込んで, photos配列へ変換していきます.

まず, PhotoData構造体をJSONと相互に変換できるように準備します.

  • PhotoData.swiftを開き, PhotoDataに Codable を追加する
    • Codable: JSONからの変換とJSONへの変換ができるデータであるという宣言
    • id = UUID()を id:String = UUID().uuidString へ変更
      • UUID型はJSONで読めないため, .uuidStringで文字列に変える

コード3

1
2
3
4
5
6
7
struct PhotoData : Identifiable, Codable {
    var id:String = UUID().uuidString
    var type:String
    var imageName:String
    var title:String
    var message:String
}

次に, JSONファイルを読み込むための処理を書きます.

  • 新規のファイルを作成, Resource.swiftに名前を変更し以下を記述する

swiftui_3_2_1_3.png

swiftui_3_2_1_4.png

  • Resource.load( name: ext: ) を作成, 指定した名前のリソースを読めるようにする
  • Bundle.main.url( forResource:withExtension ) でAppにバンドルしたファイルのURLを作成
  • Data( contentsOf: ) でURLを指定すると, 読み込んだ内容がdata変数に代入される
    • Data( contentOf: )は失敗がありうる(URLのミスなど). 失敗時に 例外 を返す
    • 例外がある処理には try をつける. 本来は例外発生時の処理を書くが今回は割愛する
    • 今回はtry!と記述し, 例外判定を無視する (※ただし例外が起きた時Appは止まる)
  • 読み込んだ結果(data変数の中身)をreturnで返却する

コード4

1
2
3
4
5
6
7
8
9
import Foundation

class Resource {
    static func load( name:String, ext:String ) -> Data {
        let url = Bundle.main.url( forResource:name, withExtension:ext )!
        let data = try! Data( contentsOf:url )
        return data
    }
}

次は再びPhotoData構造体に注目します. Resource.loadで読み込んだDataをPhotoDataに変換するコードを追加します.

  • PhotoData.swiftを開き, static func parse( json: ) を追加する
    • JSONから読み込んだDataを受け取る
    • JSONDecoder().decode( … ) で, Data型をJSONとして読むことを試みる
      • 失敗時に例外が発生するので, 今はtry!で例外を無視する
    • [PhotoData].self は変換した後の型を指定している
      • 変換後にPhotoData配列にするようdecoderに伝える

コード5

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import Foundation

struct PhotoData : Identifiable, Codable {
    var id:String = UUID().uuidString
    var type:String
    var imageName:String
    var title:String
    var message:String
    
    static func parse( json:Data ) -> [PhotoData] {
        let photos = try! JSONDecoder().decode( [PhotoData].self, from:json )
        return photos
    }
}

これで PhotoData.parse( json: ) を使うことでJSONファイルの中身を[PhotoData]に読み込めるようになりました. 次はこれを画面で使ってみます.

  • ContentView.swiftを開き, photosを空配列から以下の記述に変更する
    • Resource.load( name:ext: )で Resource/photos.json を読み込んでData型にする
    • DataをPhotoData.parse()で, [PhotoData]へ変換する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import SwiftUI

struct ContentView: View {
    @State var photos:[PhotoData] = PhotoData.parse( json:Resource.load( name:"photos", ext:"json" ) )
    
    var body: some View {
        List {
            Section {
                ForEach( $photos ) { bind_photo in
                    NavigationLink( destination:{
                        SceneryDetail( data:bind_photo )
                    },
                    label: {
                        SceneryRow( data:bind_photo.wrappedValue )
                    })
                }
                .onDelete { index_set in
                    photos.remove( atOffsets:index_set )
                }
            }
            header: {
                HStack {
                    Text( "景色写真" )
                    
                    Spacer()
                    
                    Image( systemName:"plus" )
                    .font( .title2 )
                    .foregroundStyle( Color.accentColor )
                }
            }
        }
        .listStyle( .grouped )
        .navigationTitle( "写真集" )
    
    }
}

結果

swiftui_3_2_1_5.png

読み込みに成功するとプレビューに2つのデータが表示されます. 表示されたデータはResource/photos.jsonの内容となっていることを確認してください.