初めてのSwift5 その8

画面間のデータ連携をして見よう

さて今回は画面間のデータ連携について考えてみたいと思います。
例えばA画面でデータを追加してB画面でみたいことありますよね。

例えば、ネットの買い物かごのように「コーヒー豆」を選択したら
買い物かごに「コーヒー豆」を追加したりとか、課金アイテムかったからこの画面に反映させたいとか、そういった実際のサービスとなると色々考えられるわけです。
今回はシンプルに考えたいと思うのでこんな感じ(いつもの汚い手書き)

スクリーンショット 2020-05-13 11.10.19

まず、一枚目の画面でデータの一覧を表示して先日学習たタブからアイテム追加タブを選択すると画面を切り替えてこのリストに追加するボタンー表示し、追加ボタンを追加すればこのデータに入る感じです。
とってもやりたいことシンプルですよね?

実はこれがSwiftUIだとちょっと面倒なわけです・・・他の言語になれているとですけど、初めてこの言語を習おうとした人間でも途端にハードルがあがるわけです。

そこで補足としてタブの表示構造についてもう少し突っ込んだ話をしておきます。
突然何って思うかもしれませんが、ここは丁寧に。

スクリーンショット 2020-05-13 11.30.48

今回考えている画面構成はこんな感じ。
見た目には、2枚の画面に見えるけど実は3枚もあるのでした。
リストにデータ追加するViewの名前が実際やろうとしていることと一致していないのはご愛嬌。(単純にコード書いた後で気がついたのですが気にしないでね)

各画面間のデータを受け渡す共有メモリみたいなものは、frmMain.swiftで管理します。(因みに接頭文字にfrmXXXって付け方知っている人います?バックオフィスの業務アプリ作るんだったら私が今でも最高のRADツール、最強だと思っているあの言語の話ですね(笑))

やりたいことは簡単なはずなのに、他の言語を知っていると何でこんなに面倒なのはなんでだろう的な感じですが、やってみたいと思います。

まずは管理したいデータ構造体を作る

今回はシンプルにやりたいことを実現するだけなので難しいことはしません。

//  プログラムで管理したいデータ構造体
struct ItemInfo: Codable, Equatable, Identifiable {
   //  管理する為にID
   //  Identifiableで必要。
   //  説明すると長いのでとりあえずつけとばいいかぐらいのノリでいいと思います
   var id: UUID
   //  ----- ここから下が実際にプログラムで使用するメンバー変数です -----
   var name: String    //  名前。実質これだけしか管理してません(笑)まあ分かりやすくする為にシンプルにしています
}

いきなりゴチャゴチャしてきました!
ハーン!面倒ですよね。
プロトコルというのを少し説明したいと思います。

Equatable

Pythonで言うところも「__eq__」若しくは「Equal」だと思います。
違うのかな?まあ、「=」の演算子で比較したい時に使いたいので宣言しておけばいけるぜってことです。

なれないうちは「ふ〜ん」ぐらいでいいと思います。
考えるな、感じろ!みたいな某映画みたいなノリでいいと思います。

わからなくなったら後で調べればいいのですよ。

Identifiable

こいつが厄介で、Swiftのフレームワークでデータを管理したい時に

var id: UUID

これとセットで定義する必要があります。
他の言語ではこんなのラップされて暗黙的に内部で管理している情報だと思いますがSwiftはむき出しっぽいです(笑)
※大体いきなりUUIDが何なのかなんてハードルが高い気がしますし。このぐらいはラッパーしてほしいものですね。

言い換えれば柔軟にコードがかけるし余計なものいれてないから高速化できるのだと思いますが初めてコンピュータ言語使う人は戸惑うでしょうね。

まあ、これもオマジナイだと思ってたほうがいいと思います。

今回管理したいデータ


プログラムで事実上管理しているのは

var name: String

これだけです。
今回、私達が管理したいのはこれだけだと思って下さい。
後はSwiftのフレームワークが使うフィールドなので気にしないで!

アイテムを管理する

さっきのは構造体だけで実際に全体を管理するのは以下のもの。

//  管理したいリストデータを集約して管理するクラス。
//  そうクラスなんです!構造体じゃないよ
//  管理用に関数持ちたいのでクラスにした程度でいいと思います。説明すると長いし、理論は後からで結構!
class lstItems: ObservableObject {
   //  @PubLishedは管理したいメンバーを宣言する時に使用。
   //  これ管理するぜ!ぐらいでいいと思います。
   //  小難しい理論は脇においといて作ることがモチベーションとして大事!
   @Published var item = [ItemInfo]()
   //  二次元配列にデータ構造体を追加する関数です
   //  ItemListView.swiftからボタン押した時に呼んでます
   func add(obj: ItemInfo) {
       //  その名前通り、ペコッと追加!
       item.append(obj)
   }

}

構造体ではなくクラスなんですね、ハイ。
これは、クラスの中で関数を持ちたかったからなんです。
クラスの中で管理しているメンバー変数にデータを追加するインターフェースですね。

@Publishedで宣言するとこのメンバーを管理するよっていうオマジナイです。
そう、さっき作った構造体を二次元配列で管理するメンバー変数、itemです。(sが抜けているのはご愛嬌)

でも、これも宣言だけでこれだけではまだ各画面からデータをアクセスするには不十分です。

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

   var window: UIWindow?
   //  いろんな画面でみれるようにここで宣言
   var lstitems = lstItems()

SceneDelegateってところで宣言する必要があります。
これで各シーン(画面のことだと思って下さい)で使うぜ的なやつです。

配列にデータ追加する画面を作る

今回は長いですね、やたら長い。
それだけ面倒なんです、やりたいことは簡単なのに(何度目の愚痴)

XCodeからFileメニューの→New→FileでSwiftUIを「ItemListView」でプロジェクトに追加して下さい。
※名前を適切な名前に訂正してもらっても構いません。

そして、body部分に以下のコードを追加します。

    //  グローバル変数を参照するよって意味ぐらいに覚えて下さい
   @EnvironmentObject var lstitems: lstItems

   var body: some View {
       //  縦に並べまっせ〜!
       VStack{
           //  追加ボタン
           Button(action:{
               //  ここでグローバル変数に値を追加しています
               self.lstitems.add(obj: ItemInfo(id:UUID(),name: "これはテストです(\(self.lstitems.item.count + 1)個目)") )
           }){
               //  ボタンのキャプション
               Text("追加")
           }
       }
   }

また初めての単語がでてきたと思います。

EnvironmentObject

なにかというと、グローバル変数を各画面から参照する際のオマジナイだと思って下さい。
この画面で関連するグローバル変数を触りたいって時にはこの宣言が必要になります。
毎回各画面で宣言する必要があるみたいで、面倒この上ないですがルールは
ルールですからね。
諦めてください。

配列を増やす

またボタンのところをよくみると、先程クラスで追加した関数を呼び出しているのがわかると思います。
(self.lstitems.add部分ですね。)

//  ここでグローバル変数に値を追加しています
self.lstitems.add(obj: ItemInfo(id:UUID(),name: "これはテストです(\(self.lstitems.item.count + 1)個目)") )

UUIDの部分はこんな感じでセットするぐらいに覚えてくれればいいと思います。
やっていることは単純で、「これはテストです」って文字に配列の数をセットしているだけですね。

配列は0からカウントされるので、+1して分かりやすく表示してます。

配列の一覧表示をする

配列に追加したら、中に何が入っているか知りたいですよね。
ということで、ContentViewの中に表示する画面を作ります。

   @EnvironmentObject var lstitems: lstItems
   var body: some View {
       //  縦に並べちゃうぞ!
       VStack{
           //  リスト表示するぜっ
           List{
               //  二次元配列のデータ全部ここに表示するまでグルグル回るよ
               ForEach(lstitems.item){ item in
                   HStack{
                       Text(item.name)
                       Spacer()
                   }
               }
           }
       }
   }

こんな感じで追加します。
さっきと同じ@EnvironmentObjectがでてきましたね。
そうです、この画面でもみたいですから。

ListにforEatchで配列に入っている全てのものを表示する簡単なプログラムです。
やっていることは単純でしょ?

メイン画面を作成する

さてようやくここで、メイン画面を作りたいと思います。
最初の手書きの図で描いた「frmMain.swift」ですね。

XCodeからFileメニューの→New→FileでSwiftUIを「frmMain」でプロジェクトに追加して下さい。

        //  タブ宣言!
       TabView{
           //  画面のその1
           //  ディフォルトでこの画面を表示するよってことです。
           //  ContentView.swiftの内容ね
           ContentView()
               //  タブのアイテムとして以下を追加
               .tabItem{
                   //  前回と同じ
               Image(systemName: "list.dash")
               Text("リスト表示")
           }
           //  画面その2
           //  管理したいデータをこの画面で追加します
           ItemListView()
               //  タブアイテムとして以下を追加
               .tabItem{
                   //  このあたりも前回と同じ
                   Image(systemName: "square.and.pencil")
                   Text("アイテム追加")
           }
       }

こんな感じでbody部分に追加します。
前回やった通りにタブを作成し先程作成した画面をここで表示するように設定しています。

あれ、EnvironmentObjectがでてこないじゃないかと思う貴方、いい感してますね。
コードみてもらうと分かりますが、タブと画面表示のみでグローバル変数を使用していないからここでは記載していません。

ですが、以下のところに記載しています。

struct frmMain_Previews: PreviewProvider {
    //  グローバル変数の実体を管理
   //  この画面で、データ構造体を管理します
   static let lstitems = lstItems()

   static var previews: some View {
       frmMain()
       //  データ構造体を渡します。
       //  SceneDelegateで追加したあれ。
       .environmentObject(lstitems)
   }
}

手書きの図で説明しましたが、実はこのViewでグローバル変数の実体を管理していたりします。
か〜ややこしい!もっといい方法はまだこの言語触って一週間と少しの私では知りません。

後、environmentObjectとして宣言したオブジェクトを渡していますがそれは次の章で説明します。

スタートアップViewを変更する

スタートアップで表示するViewを変更したいと思います。
SceneDelegateの設定を変更します。

//  オリジナルはコメントアウト。これ基本。
//        let contentView = ContentView()

//  frmMainをスタートアップに変更
//  で、グローバル変数を画面に渡します
let contentView = frmMain().environmentObject(lstitems)  

ディフォルトでは、ContentView()を先に表示する設定になっているのですがそれを今回作成したfrmMainに変更します。
また、先程追加したenvironmentObjectと同じようにグローバル変数の実体部分をViewに受け渡しています。

実行してみよう

お疲れ様でした。
長かったですが、これで実行で切る準備がようやくできたんです(感無量)

スクリーンショット 2020-05-13 15.42.01

実行するとこんな感じの画面がでてきます。
最初の手書きイメージと一致していますね。

リストには何も表示されていません。
これはまだ何も追加していないからです。
では、アイテム追加のタブをクリックして画面を切り替えてみましょう。

スクリーンショット 2020-05-13 15.43.34

これも手書きイメージと同じようにボタン一個のシンプルな画面です。
ボタンを押して配列を増やしてみましょう!
ボタンを押した後に、再度リスト表示タブを選択するとどうでしょうか?

スクリーンショット 2020-05-13 15.44.44

正しく配列が追加されましたね。
ということで、先程のアイテム追加の画面から連携された情報がリスト表示画面に戻って正しく受け渡されていることが確認できました!

これで一般的なデータの受け渡しを学ぶことができたということです。
ちょっと正直、ハードルが高いですがここを乗り越えないと先に進めないので解説して置きました。

私も、すんなり覚えたわけではなく調べながら実施していました。
こう書いててなんですが有益な情報は結構海外サイトに落ちていることが多いので英語のドキュメントよんだり事例をみたりしていました。

なので、ここは理解できるまで繰り返し覚えたほうが良さげです。
では、また。

今回の全コード

frmMain.swift

//
//  frmMain.swift
//  HogeHoge7
//
//  Created by melon on 2020/05/12.
//  Copyright © 2020 melon-group. All rights reserved.
//

import SwiftUI

//  起動時にこの画面がトップにでるよ!
struct frmMain: View {
   var body: some View {
       //  タブ宣言!
       TabView{
           //  画面のその1
           //  ディフォルトでこの画面を表示するよってことです。
           //  ContentView.swiftの内容ね
           ContentView()
               //  タブのアイテムとして以下を追加
               .tabItem{
                   //  前回と同じ
               Image(systemName: "list.dash")
               Text("リスト表示")
           }
           //  画面その2
           //  管理したいデータをこの画面で追加します
           ItemListView()
               //  タブアイテムとして以下を追加
               .tabItem{
                   //  このあたりも前回と同じ
                   Image(systemName: "square.and.pencil")
                   Text("アイテム追加")
           }
       }
   }
}

struct frmMain_Previews: PreviewProvider {
   //  グローバル変数の実体を管理
   //  この画面で、データ構造体を管理します
   static let lstitems = lstItems()

   static var previews: some View {
       frmMain()
       //  データ構造体を渡します。
       //  SceneDelegateで追加したあれ。
       .environmentObject(lstitems)
   }
}

lstItems.swift

//
//  lstItems.swift
//  HogeHoge7
//
//  Created by melon on 2020/05/12.
//  Copyright © 2020 melon-group. All rights reserved.
//

import SwiftUI

//  管理したいリストデータを集約して管理するクラス。
//  そうクラスなんです!構造体じゃないよ
//  管理用に関数持ちたいのでクラスにした程度でいいと思います。説明すると長いし、理論は後からで結構!
class lstItems: ObservableObject {
   //  @PubLishedは管理したいメンバーを宣言する時に使用。
   //  これ管理するぜ!ぐらいでいいと思います。
   //  小難しい理論は脇においといて作ることがモチベーションとして大事!
   @Published var item = [ItemInfo]()
   //  二次元配列にデータ構造体を追加する関数です
   //  ItemListView.swiftからボタン押した時に呼んでます
   func add(obj: ItemInfo) {
       //  その名前通り、ペコッと追加!
       item.append(obj)
   }

}

ItemInfo.swift

//
//  lstData.swift
//  HogeHoge7
//
//  Created by melon on 2020/05/12.
//  Copyright © 2020 melon-group. All rights reserved.
//

import SwiftUI

//  プログラムで管理したいデータ構造体
struct ItemInfo: Codable, Equatable, Identifiable {
   //  管理する為にID
   //  Identifiableで必要。
   //  説明すると長いのでとりあえずつけとばいいかぐらいのノリでいいと思います
   var id: UUID
   //  ----- ここから下が実際にプログラムで使用するメンバー変数です -----
   var name: String    //  名前。実質これだけしか管理してません(笑)まあ分かりやすくする為にシンプルにしています
}

ItemListView.swift

//
//  ItemListView.swift
//  HogeHoge7
//
//  Created by melon on 2020/05/12.
//  Copyright © 2020 melon-group. All rights reserved.
//

import SwiftUI

//  作成した後気がついたけど名前と実際やっていることが一致していないことは内緒(笑)
//  ごめん、面倒なのでそのままにしてます。
struct ItemListView: View {
   //  グローバル変数を参照するよって意味ぐらいに覚えて下さい
   @EnvironmentObject var lstitems: lstItems

   var body: some View {
       //  縦に並べまっせ〜!
       VStack{
           //  追加ボタン
           Button(action:{
               //  ここでグローバル変数に値を追加しています
               self.lstitems.add(obj: ItemInfo(id:UUID(),name: "これはテストです(\(self.lstitems.item.count + 1)個目)") )
           }){
               //  ボタンのキャプション
               Text("追加")
           }
       }
   }
}

struct ItemListView_Previews: PreviewProvider {
   static var previews: some View {
       ItemListView()
   }
}

SceneDelegate.swift

//
//  SceneDelegate.swift
//  HogeHoge7
//
//  Created by melon on 2020/05/12.
//  Copyright © 2020 melon-group. All rights reserved.
//

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

   var window: UIWindow?
   //  いろんな画面でみれるようにここで実体宣言
   var lstitems = lstItems()

   func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
       // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
       // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
       // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

       // Create the SwiftUI view that provides the window contents.

       //  オリジナルはコメントアウト。これ基本。
       //        let contentView = ContentView()

       //  frmMainをスタートアップに変更
       //  で、グローバル変数を画面に渡します
       let contentView = frmMain().environmentObject(lstitems)
       
       // Use a UIHostingController as window root view controller.
       if let windowScene = scene as? UIWindowScene {
           let window = UIWindow(windowScene: windowScene)
           window.rootViewController = UIHostingController(rootView: contentView)
           self.window = window
           window.makeKeyAndVisible()
       }
   }

   func sceneDidDisconnect(_ scene: UIScene) {
       // Called as the scene is being released by the system.
       // This occurs shortly after the scene enters the background, or when its session is discarded.
       // Release any resources associated with this scene that can be re-created the next time the scene connects.
       // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
   }

   func sceneDidBecomeActive(_ scene: UIScene) {
       // Called when the scene has moved from an inactive state to an active state.
       // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
   }

   func sceneWillResignActive(_ scene: UIScene) {
       // Called when the scene will move from an active state to an inactive state.
       // This may occur due to temporary interruptions (ex. an incoming phone call).
   }

   func sceneWillEnterForeground(_ scene: UIScene) {
       // Called as the scene transitions from the background to the foreground.
       // Use this method to undo the changes made on entering the background.
   }

   func sceneDidEnterBackground(_ scene: UIScene) {
       // Called as the scene transitions from the foreground to the background.
       // Use this method to save data, release shared resources, and store enough scene-specific state information
       // to restore the scene back to its current state.
   }


}

ContentView.swift

//
//  ContentView.swift
//  HogeHoge7
//
//  Created by melon on 2020/05/12.
//  Copyright © 2020 melon-group. All rights reserved.
//

import SwiftUI

//  この画面で、グローバル変数に追加された構造体をリスト表示してます。
struct ContentView: View {
   @EnvironmentObject var lstitems: lstItems
   var body: some View {
       //  縦に並べちゃうぞ!
       VStack{
           //  リスト表示するぜっ
           List{
               //  二次元配列のデータ全部ここに表示するまでグルグル回るよ
               ForEach(lstitems.item){ item in
                   HStack{
                       Text(item.name)
                       Spacer()
                   }
               }
           }
       }
   }
}

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


この記事が気に入ったらサポートをしてみませんか?