質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Firebase

Firebaseは、Googleが提供するBasSサービスの一つ。リアルタイム通知可能、並びにアクセス制御ができるオブジェクトデータベース機能を備えます。さらに認証機能、アプリケーションのログ解析機能などの利用も可能です。

iOS

iOSとは、Apple製のスマートフォンであるiPhoneやタブレット端末のiPadに搭載しているオペレーションシステム(OS)です。その他にもiPod touch・Apple TVにも搭載されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

Q&A

解決済

1回答

343閲覧

SwiftUIで、Firestoreから取得した構造体配列データをリスト(List)に反映できない

yoheionishi

総合スコア5

Firebase

Firebaseは、Googleが提供するBasSサービスの一つ。リアルタイム通知可能、並びにアクセス制御ができるオブジェクトデータベース機能を備えます。さらに認証機能、アプリケーションのログ解析機能などの利用も可能です。

iOS

iOSとは、Apple製のスマートフォンであるiPhoneやタブレット端末のiPadに搭載しているオペレーションシステム(OS)です。その他にもiPod touch・Apple TVにも搭載されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

0グッド

0クリップ

投稿2024/02/12 12:08

編集2024/02/13 14:53

実現したいこと

ここに実現したいことを箇条書きで書いてください。

  • Swift UIで、Firestoreから取得したデータを2次元配列に格納して、List表示する
  • @Observableを利用して、取得データを適切に画面反映する

前提

Itemという構造体を定義し、Firestore上にそのデータを保持しています。Itemは、アイテム名の他に、カテゴリー名と紐づくID(Int)を持っています。
アプリ側では、「Item配列」としてデータを取得した後、カテゴリーIDごとにアイテムを並び替え、セクション名=カテゴリー名となるようにリスト表示します。
FirestoreデータはListService内で読み込む関数を定義し(func readItems())、そのコールバックの呼び出し元をListViewModelに定義しています。

構成

  • ListView : Itemを表示するSwift UI View
  • ListViewModel: データの加工や並び替えを主に行うクラス
  • ListService: データ取得を行うクラス
  • Item: 構造体クラス

ターゲット

  • iOS17.2

発生している問題・エラーメッセージ

ListViewModel内で、リスト表示用の2次元配列を生成し、ListViewで表示しようとしていますが、どうしてもデータ取得完了前に画面描画が完了してしまうようで、リストが表示されません。
(2次元配列へのデータ格納はできていることを確認済みです)
ListViewModelを@Observationで定義し、ListView内で@Stateでインスタンス化することで、反映させようとしていますがうまく動作してくれません。
ForEachで2次元配列を表示する部分については問題なさそうなので、データの画面反映の部分の問題だと考えております。
2、3日情報を集めたり修正したりしましたが、うまく突破できず、お力をお貸しいただけると本当に助かります。
どうぞよろしくお願いいたします。

エラーメッセージ

該当のソースコード

以下が、該当コードです。投稿時に変数名を修正したので一部間違えている可能性がありますがご容赦ください・・

Item.swift

1 2import Foundation 3import FirebaseFirestoreSwift 4 5struct Item: Codable, Identifiable, Hashable { 6 7 @DocumentID var id: String? 8 9 var brandid : String 10 var brandname : String 11 var categoryid : Int 12 var categoryname : String 13 var createdAt : Date //作成日 14 var itemid : String 15 var memo : String 16 var name : String 17 var subcategoryid : Int 18 var subcategoryname : String 19 var updatedAt : Date //更新日 20}

ItemService.swift

1 2import Foundation 3import FirebaseFirestore 4import FirebaseFirestoreSwift 5 6struct ItemService { 7 8 func readItems(list:List, completion: @escaping([Item]) -> Void) { 9 10 var items = [Item]() 11 12 //Listの"listitems"から、itemidだけを抜き出した配列を作る 13 let listItems:[listItem] = List.listitems ?? [] 14 let itemIds = listItems.map ( { (st) -> String in 15 return st.itemid 16 }) 17 18 let docRef = Firestore.firestore().collection("users").document("userid10001").collection("item") 19 20 //itemIdsに入っているitemidを使って、リストに含まれるアイテムの詳細情報を読む 21 docRef.whereField("itemid", in: itemIds).getDocuments() { querySnapshot, error in 22 if let error = error { 23 print("Error getting document: \(error)") 24 } else { 25 for documentChange in querySnapshot!.documentChanges { 26 if documentChange.type == .added { 27 do { 28 let item = try documentChange.document.data(as: Item.self) 29 items.append(item) 30 } catch { 31 print(error.localizedDescription) 32 } 33 } 34 } 35 } 36 completion(items) 37 } 38 } 39

ListViewModel.swift

1 2import Foundation 3import FirebaseFirestore 4import Observation 5import Collections 6 7@Observable class ListViewModel { 8 9 private var list : List = List(listid: "1", listname: "", lengthof: "", howto: "", startdate: Date(), baseweight: 0, consumableweight: 0, wornweight: 0, totalweight: 0, listitems: [listItem]()) 10 11 private let itemservice = ItemService() 12 13 var listData: [[Item]] = [] 14 15 /////////////////////////// クラス初期化(引数なし) /////////////////////// 16 init() { 17 } 18 19 /////////////////////////// クラス初期化(引数あり) /////////////////////// 20 init(list: List) { 21 self.list = List 22 readAndSortItems(list: List) 23 } 24 25 func readAndSortItems(list: List) -> Void { 26 27 itemservice.readItems(list: List) { (items) in 28 29 let items = items 30 31 //1.categoryID(Int)だけを取り出して新しい配列を作成 32 let categoryIds = items.map { (item) -> Int in 33 return item.categoryid 34 } 35 36 //2.重複削除 37 let uniqueCategoryIds = OrderedSet(categoryIds) 38 39 //3.categoryIDの小さい順に並び替え 40 let sortedCategoryIds = uniqueCategoryIds.sorted{ $0 < $1 } 41 42 //List表示用の2次元配列を作成(要素はItemクラス) 43 //sortedCategoryIdsの中のカテゴリーID順に、itemArrayを検索して、当てはまるものを順番に2次元配列に入れる 44 for (_, categoyIdNum) in sortedCategoryIds.enumerated() { 45 let itemArray = items.filter( {$0.categoryid == categoyIdNum} ) 46 self.listData.append(itemArray) 47 } 48 } 49 } 50

ListView.swift

1 2import SwiftUI 3import Observation 4 5struct ListView: View { 6 7 var list: List 8 //viewModelプロパティに@Stateを付与して値監視 9 @State private var listVM = ListViewModel() 10 @Binding var path: [List] 11 12 init(list: List, path: Binding<[List]>) { 13 self.list = list 14 self._path = path 15 self.ListVM = ListViewModel(list: List) 16 } 17 18 19 var body: some View { 20 let _ = Self._printChanges() 21 VStack { 22 23 ZStack { 24 Color.init(uiColor: UIColor.secondarySystemBackground).edgesIgnoringSafeArea(.all) 25 26 List() { 27 28 ForEach(ListVM.listData.indices, id: \.self) { index in 29 Section(header: Text(ListVM.listData[index][0].categoryname)) { 30 ForEach(ListVM.listData[index].indices, id: \.self) { index2 in 31 //Text(listData[index].count) 32 Text(ListVM.listData[index][index2].name) 33 34 } 35 } 36 } 37 } 38 } 39 } 40 } 41 42} 43

追記:Firestore記述を削除したコード全体

code全体(修正版)

1 2// ContentView.swift 3import SwiftUI 4struct ContentView: View { 5 let gearList = 6 GearList( 7 gearlistid: "gearlistid", 8 listname: "gearlistname", 9 lengthofstay: "2day", 10 howtostay: "Hotel", 11 startdate: Date(), 12 baseweight: 100, 13 consumableweight: 100, 14 wornweight: 100, 15 totalweight: 300 16 ) 17 var body: some View { 18 ListView(gearList: gearList) 19 } 20} 21 22#Preview { 23 ContentView() 24} 25 26 27// Item.swift 28import Foundation 29struct Item: Codable, Identifiable, Hashable { 30 var id: String? 31 var brandid: String 32 var brandname: String 33 var categoryid: Int 34 var categoryname: String 35 var createdAt: Date 36 var itemid: String 37 var memo: String 38 var name: String 39 var subcategoryid: Int 40 var subcategoryname: String 41 var updatedAt: Date 42} 43 44// GearList.swift 45import Foundation 46struct GearList: Codable, Identifiable, Hashable { 47 var id: String? 48 var gearlistid : String 49 var listname : String 50 var lengthofstay : String 51 var howtostay : String 52 var startdate : Date 53 var baseweight : Int 54 var consumableweight : Int 55 var wornweight : Int 56 var totalweight : Int 57} 58 59 60// ItemService.swift 61import Foundation 62struct ItemService { 63 func readItems(gearList:GearList, completion: @escaping([Item]) -> Void) { 64 let items: [Item] = [ 65 Item(brandid: "brandid", 66 brandname: "brandname", 67 categoryid: 11, 68 categoryname: "categoryname", 69 createdAt: Date(), 70 itemid: "itemid", 71 memo: "memo", 72 name: "name\(Int.random(in: 0..<100))", 73 subcategoryid: 12, 74 subcategoryname: "subcategoryname", 75 updatedAt: Date()), 76 Item(brandid: "brandid", 77 brandname: "brandname", 78 categoryid: 11, 79 categoryname: "categoryname", 80 createdAt: Date(), 81 itemid: "itemid", 82 memo: "memo", 83 name: "name\(Int.random(in: 0..<100))", 84 subcategoryid: 12, 85 subcategoryname: "subcategoryname", 86 updatedAt: Date()), 87 ] 88 completion(items) 89 } 90} 91 92// ListViewModel.swift 93import Foundation 94import Observation 95 96@Observable class ListViewModel { 97 private let itemservice = ItemService() 98 private var gearList : GearList 99 var listData: [[Item]] = [] 100 init() { 101 print("init") 102 self.gearList = GearList(gearlistid: "", listname: "", lengthofstay: "", howtostay: "", startdate: Date(), baseweight: 0, consumableweight: 0, wornweight: 0, totalweight: 0) 103 } 104 init(gearList:GearList) { 105 self.gearList = gearList 106 readAndSortItems(gearList:gearList) 107 } 108 func readAndSortItems(gearList:GearList) -> Void { 109 listData = [] 110 itemservice.readItems(gearList:gearList) { item in 111 print("completion") 112 self.listData.append(item) 113 } 114 } 115} 116 117 118// ListView.swift 119import SwiftUI 120import Observation 121struct ListView: View { 122 var gearList: GearList 123 @State private var listVM = ListViewModel() 124 125 init(gearList: GearList) { 126 self.gearList = gearList 127 self.listVM = ListViewModel(gearList: gearList) 128 } 129 130 var body: some View { 131 Button("tap", action: action) 132 List { 133 ForEach(listVM.listData.indices, id: \.self) { index in 134 Section(header:Text(listVM.listData[index][0].categoryname)) { 135 ForEach(listVM.listData[index].indices, id: \.self) { index2 in 136 Text(listVM.listData[index][index2].name) 137 } 138 } 139 } 140 } 141 } 142 func action() { 143 listVM.readAndSortItems(gearList: gearList) 144 } 145} 146 147

試したこと

  • @Observation宣言の有無や、@Stateも試してみたもののうまくいかずでした

補足情報(FW/ツールのバージョンなど)

xcode 15.2

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

ごめんなさい。回答ではないのですが、コードを記載したかったため、回答欄に入力します。

iOS17.0からObservationというのが使えるようになったのですね。
勉強になります。

投稿時に変数名を修正したので一部間違えている可能性がありますがご容赦ください・・

ちょっと該当のソースコードのままだと、List型でエラーが多く出て、厳しい感じです。
何をしようとしているのか把握するのも難しいです。

(2次元配列へのデータ格納はできていることを確認済みです)
ForEachで2次元配列を表示する部分については問題なさそうなので、データの画面反映の部分の問題だと考えております。

Firestoreが影響していないように見えますので、
問題を切り分けるためにも、該当のソースコードにFirestoreの部分を削除したものもご記載いただけますでしょうか。
(Forestoreを使っているとこちらでの再現確認がちょっと難しいです・・)
(Forestoreから取得するデータの部分をソースコードに直接記述するイメージで該当のソースコードを書いてもらえると助かります。)

こちらで不要そうなコードを削除してObservationの動きを試してみたところ、
ListViewModel.listDataの内容は画面に表示できているように見えました。

*コピペで動かせると思いますので、これをベースに問題を再現できるようにできますでしょうか?

swift

1import SwiftUI 2 3struct ContentView: View { 4 var body: some View { 5 ListView() 6 } 7} 8 9#Preview { 10 ContentView() 11} 12 13 14// Item.swift 15import Foundation 16struct Item: Codable, Identifiable, Hashable { 17 var id: String? 18 var brandid: String 19 var brandname: String 20 var categoryid: Int 21 var categoryname: String 22 var createdAt: Date 23 var itemid: String 24 var memo: String 25 var name: String 26 var subcategoryid: Int 27 var subcategoryname: String 28 var updatedAt: Date 29} 30 31// ItemService.swift 32import Foundation 33struct ItemService { 34 func readItems(completion: @escaping([Item]) -> Void) { 35 let items: [Item] = [ 36 Item(brandid: "brandid", 37 brandname: "brandname", 38 categoryid: 11, 39 categoryname: "categoryname", 40 createdAt: Date(), 41 itemid: "itemid", 42 memo: "memo", 43 name: "name\(Int.random(in: 0..<100))", 44 subcategoryid: 12, 45 subcategoryname: "subcategoryname", 46 updatedAt: Date()), 47 Item(brandid: "brandid", 48 brandname: "brandname", 49 categoryid: 11, 50 categoryname: "categoryname", 51 createdAt: Date(), 52 itemid: "itemid", 53 memo: "memo", 54 name: "name\(Int.random(in: 0..<100))", 55 subcategoryid: 12, 56 subcategoryname: "subcategoryname", 57 updatedAt: Date()), 58 ] 59 completion(items) 60 } 61} 62 63// ListViewModel.swift 64import Foundation 65import Observation 66@Observable class ListViewModel { 67 private let itemservice = ItemService() 68 var listData: [[Item]] = [] 69 init() { 70 readAndSortItems() 71 } 72 func readAndSortItems() -> Void { 73 listData = [] 74 itemservice.readItems() { item in 75 print("completion") 76 self.listData.append(item) 77 } 78 } 79} 80 81// ListView.swift 82import SwiftUI 83import Observation 84struct ListView: View { 85 @State private var listVM = ListViewModel() 86 var body: some View { 87 Button("tap", action: action) 88 List { 89 ForEach(listVM.listData.indices, id: \.self) { index in 90 Section(header:Text(listVM.listData[index][0].categoryname)) { 91 ForEach(listVM.listData[index].indices, id: \.self) { index2 in 92 Text(listVM.listData[index][index2].name) 93 } 94 } 95 } 96 } 97 } 98 func action() { 99 listVM.readAndSortItems() 100 } 101}

追記です。

コードの修正ありがとうございます。
再現確認できました。

原因がわかりました。
self.listVM = ListViewModel(gearList: gearList) でlistVMを設定していますが、
この方法では@Stateとしてちゃんと設定されていないことになるみたいです。

次のリンク先の「イニシャライザで値を設定する場合」を見てみると良いと思います。
【SwiftUI】@Stateの使い方 | カピ通信

ListViewModelのイニシャライザと、
listVMの初期値は不要になりそうですね。

修正したコードです。
修正ポイントに// ***のようなコメントを入れましたので、見てみてください。

swift

1// ContentView.swift 2import SwiftUI 3struct ContentView: View { 4 let gearList = 5 GearList( 6 gearlistid: "gearlistid", 7 listname: "gearlistname", 8 lengthofstay: "2day", 9 howtostay: "Hotel", 10 startdate: Date(), 11 baseweight: 100, 12 consumableweight: 100, 13 wornweight: 100, 14 totalweight: 300 15 ) 16 var body: some View { 17 ListView(gearList: gearList) 18 } 19} 20 21#Preview { 22 ContentView() 23} 24 25 26// Item.swift 27import Foundation 28struct Item: Codable, Identifiable, Hashable { 29 var id: String? 30 var brandid: String 31 var brandname: String 32 var categoryid: Int 33 var categoryname: String 34 var createdAt: Date 35 var itemid: String 36 var memo: String 37 var name: String 38 var subcategoryid: Int 39 var subcategoryname: String 40 var updatedAt: Date 41} 42 43// GearList.swift 44import Foundation 45struct GearList: Codable, Identifiable, Hashable { 46 var id: String? 47 var gearlistid : String 48 var listname : String 49 var lengthofstay : String 50 var howtostay : String 51 var startdate : Date 52 var baseweight : Int 53 var consumableweight : Int 54 var wornweight : Int 55 var totalweight : Int 56} 57 58 59// ItemService.swift 60import Foundation 61struct ItemService { 62 func readItems(gearList:GearList, completion: @escaping([Item]) -> Void) { 63 let items: [Item] = [ 64 Item(brandid: "brandid", 65 brandname: "brandname", 66 categoryid: 11, 67 categoryname: "categoryname", 68 createdAt: Date(), 69 itemid: "itemid", 70 memo: "memo", 71 name: "name\(Int.random(in: 0..<100))", 72 subcategoryid: 12, 73 subcategoryname: "subcategoryname", 74 updatedAt: Date()), 75 Item(brandid: "brandid", 76 brandname: "brandname", 77 categoryid: 11, 78 categoryname: "categoryname", 79 createdAt: Date(), 80 itemid: "itemid", 81 memo: "memo", 82 name: "name\(Int.random(in: 0..<100))", 83 subcategoryid: 12, 84 subcategoryname: "subcategoryname", 85 updatedAt: Date()), 86 ] 87 completion(items) 88 } 89} 90 91// ListViewModel.swift 92import Foundation 93import Observation 94 95@Observable class ListViewModel { 96 private let itemservice = ItemService() 97 private var gearList : GearList 98 var listData: [[Item]] = [] 99 // *** init()コメントアウト 100// init() { 101// print("init") 102// self.gearList = GearList(gearlistid: "", listname: "", lengthofstay: "", howtostay: "", startdate: Date(), baseweight: 0, consumableweight: 0, wornweight: 0, totalweight: 0) 103// } 104 init(gearList:GearList) { 105 self.gearList = gearList 106 readAndSortItems(gearList:gearList) 107 } 108 func readAndSortItems(gearList:GearList) -> Void { 109 listData = [] 110 itemservice.readItems(gearList:gearList) { item in 111 print("completion") 112 self.listData.append(item) 113 } 114 } 115} 116 117 118// ListView.swift 119import SwiftUI 120import Observation 121struct ListView: View { 122 var gearList: GearList 123 // *** 初期値なし 124 @State private var listVM: ListViewModel 125 126 init(gearList: GearList) { 127 self.gearList = gearList 128 // *** @Stateをinit()で設定する場合は少し特殊な記述になります。 129 _listVM = State(initialValue: ListViewModel(gearList: gearList)) 130 } 131 132 var body: some View { 133 Button("tap", action: action) 134 List { 135 ForEach(listVM.listData.indices, id: \.self) { index in 136 Section(header:Text(listVM.listData[index][0].categoryname)) { 137 ForEach(listVM.listData[index].indices, id: \.self) { index2 in 138 Text(listVM.listData[index][index2].name) 139 } 140 } 141 } 142 } 143 } 144 func action() { 145 listVM.readAndSortItems(gearList: gearList) 146 } 147}

投稿2024/02/13 09:58

編集2024/02/14 09:51
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yoheionishi

2024/02/13 14:50

ご回答ありがとうございます! 作成いただいたコードをもとに、こちらの手元にある問題のあるコードを、Firestoreの記述を抜いた形で追記いたしました。GearListというクラスを使っていたため、そちらも追記しています。 こちらを動かした時の挙動は、 1.リストが何も表示されない 2. Tapボタンを押すと、リストが表示される となり、当初私からご質問させていただいたのと同じ状態が再現できています。 いただいたコードからの変更点としては、gearList:GearList をListViewModel→ItemServiceと渡しており、そのための記述を追記しています。(おそらくその際に、誤った引数の渡し方や初期化をしているのが原因なのではないかと・・) 大変お手数ですが、改めてご確認いただけると大変助かります。 どうぞよろしくお願いいたします。
yoheionishi

2024/02/15 14:33

ご回答ありがとうございます。 お返事遅くなってしまい失礼しました。 いただいた修正ポイントを元のコードに反映し、正常に動作するようになりました! @Stateをイニシャライザで設定する際のルールがあることを理解しておらず、ご指摘いただいて大変助かりました。 また、最初のコード修正も大変参考になりました。 ありがとうございました!!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問