画面遷移先でグローバルなオブジェクトのメンバを変更する

アプリケーションの動作パラメータを設定する場合、別の画面にモーダル遷移して値を設定するシチュエーションが多いと思います。これを実現する方法を紹介します。この方法がベストかどうかわかりませんがシンプルでわかりやすいと思います。

Fig. 1 起動直後
Fig. 2 モーダル遷移で全パラメータを参照
Fig. 3 ステッパボタンで値を増減
Fig. 4 元の画面に戻ってきた

下記が管理したいパラメータです。ParametersSystemOperation というのがグローバルに扱いたいクラスで、そこには ParametersMemberAtoF という整数(Int)をメンバに持つクラスと、ParametersMemberUtoZ という小数(Double)をメンバに持つクラスを抱えているとします。

import Foundation

class ParametersMemberAtoF {

    var MemberA = 0
    var MemberB = 0
    var MemberC = 0
    var MemberD = 0
    var MemberE = 0
    var MemberF = 0

    init(){
        MemberA = 10
        MemberB = 20
        MemberC = 30
        MemberD = 40
        MemberE = 50
        MemberF = 60
    }

}

class ParametersMemberUtoZ {

    var MemberU = 0.0
    var MemberV = 0.0
    var MemberW = 0.0
    var MemberX = 0.0
    var MemberY = 0.0
    var MemberZ = 0.0

    init(){
        MemberU =      1.0
        MemberV =     10.0
        MemberW =    100.0
        MemberX =   1000.0
        MemberY =  10000.0
        MemberZ = 100000.0
    }
}

class ParametersSystemOperation: ObservableObject {

    @Published var PrmsAtoF: ParametersMemberAtoF
    @Published var PrmsUtoZ: ParametersMemberUtoZ

    init (){
        PrmsAtoF = ParametersMemberAtoF()
        PrmsUtoZ = ParametersMemberUtoZ()
    }

}

次に、プロジェクト名を aaa とした場合は、aaaApp.swift というファイルがデフォルトで存在するはずなので、そこで ContentView クラス以下でグローバルに扱えるクラスとして ParametersSystemOperation のインスタンスを登録します。

import SwiftUI

@main
struct aaaApp: App {
    var body: some Scene {
        WindowGroup {

            // パラメータを持つクラスを関連づける.
            ContentView()
                .environmentObject( ParametersSystemOperation() )

        }
    }
}

下記が ContentView です、つまりトップレベルの画面となります。そこでは @EnvironmentObject var Ps: ParametersSystemOperation という変数を定義します。これが aaaApp.swift において関連づけたグローバルなクラスへの参照になります。

@EnvironmentObject で参照した変数は @State な画面更新と関連付けられた変数ではないので、Ps で参照した先のメンバ変数が変更されても、画面の更新はされません。そこで仕方なく @State な変数 memberA と memberU を定義します。

画面が更新されなければいけないタイミングは2箇所です。画面が最初に表示されたとき、モーダル遷移画面から戻ってきたとき、の2箇所です。

58、59行目が、画面が最初に表示されたときに実行されるコードです。
50、51行目が、モーダル遷移画面から戻ってきたときに実行されるコードです。

import SwiftUI

struct ContentView: View {

    @EnvironmentObject var Ps: ParametersSystemOperation

    @State var FlagShow = false

    let TXT_SIZE = 20.0
    let TXT_PADDING = 5.0

    @State var memberA: Int = 0
    @State var memberU: Double = 0.0

    var body: some View {

        ZStack {

            Color( .lightGray )

            VStack{

                Spacer()

                Text( String( format: "A: %d", memberA ) )
                    .foregroundColor( .white )


                Text( String( format: "U: %f", memberU ) )
                    .foregroundColor( .white )

                Spacer()

                Button( action:{
                    FlagShow = true
                })
                {
                    Text( "parameters" )
                        .font( .system( size: TXT_SIZE ) )
                        .foregroundColor( .white )
                }
                .padding( TXT_PADDING )
                .background( .blue )

                Spacer()

            }
            .sheet( isPresented: $FlagShow, onDismiss: {
                // モーダルから Dismiss で戻ってきたときの処理.
                memberA = Ps.PrmsAtoF.MemberA
                memberU = Ps.PrmsUtoZ.MemberU
            }) {
                View000()
            }

        }.onAppear() {
            // 画面が表示されたときの処理.
            memberA = Ps.PrmsAtoF.MemberA
            memberU = Ps.PrmsUtoZ.MemberU
        }


    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {

        // aaaApp.swift と同様に、プレビューコードとしてパラメータを持つクラスを関連づける.
        ContentView()
            .environmentObject( ParametersSystemOperation() )

    }
}

下記がモーダル遷移先で表示される画面 View000 のコードです。

Text を Stepper で囲んで、値をボタンで増減できるようにしました。このビューでも @EnvironmentObject の値は @State な値ではありませんので、仕方なく @State な変数を多数定義します。

こちらはすべての変数を画面更新したいので、大量の @State 変数を宣言しなくてはなりません。

@State な変数をグローバルなクラスへの参照 Ps のメンバに代入するタイミング、また、その逆は2箇所あります。

87〜103行目、このビューが表示されたときに Ps の値を @State な変数に代入します。
58〜70行目、ボタンが押されたときに @State な変数の値を、Ps のメンバに代入します。その後、画面を閉じて元に戻ります(Dismiss)。

ボタンを押さずに画面を下にスワイプさせて閉じた場合は、Ps のメンバは変更されません。

import SwiftUI

struct View000: View {

    @EnvironmentObject var Ps: ParametersSystemOperation

    @Environment( \.dismiss ) var my_dismiss_action

    let BTN_TXT_SIZE = 20.0
    let BTN_TXT_PADDING = 10.0

    let LIST_TXT_SIZE = 12.0

    @State var memberA :Int = 0
    @State var memberB :Int = 0
    @State var memberC :Int = 0
    @State var memberD :Int = 0
    @State var memberE :Int = 0
    @State var memberG :Int = 0

    @State var memberU :Double = 0.0
    @State var memberV :Double = 0.0
    @State var memberW :Double = 0.0
    @State var memberX :Double = 0.0
    @State var memberY :Double = 0.0
    @State var memberZ :Double = 0.0

    var body: some View {

        VStack {

            Spacer()

            List {
                Stepper(value: $memberA, in: 0 ... 1024, step: 1 ){ Text( String( format: "A: %d", memberA )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberB, in: 0 ... 1024, step: 1 ){ Text( String( format: "B: %d", memberB )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberC, in: 0 ... 1024, step: 1 ){ Text( String( format: "C: %d", memberC )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberD, in: 0 ... 1024, step: 1 ){ Text( String( format: "D: %d", memberD )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberE, in: 0 ... 1024, step: 1 ){ Text( String( format: "E: %d", memberE )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberG, in: 0 ... 1024, step: 1 ){ Text( String( format: "F: %d", memberG )).font( .system( size: LIST_TXT_SIZE )) }
            }

            Spacer()

            List {
                Stepper(value: $memberU, in: 0 ... 999999.9, step: 0.01 ){ Text( String( format: "U: %f", memberU )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberV, in: 0 ... 999999.9, step: 0.01 ){ Text( String( format: "V: %f", memberV )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberW, in: 0 ... 999999.9, step: 0.01 ){ Text( String( format: "W: %f", memberW )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberX, in: 0 ... 999999.9, step: 0.01 ){ Text( String( format: "X: %f", memberX )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberY, in: 0 ... 999999.9, step: 0.01 ){ Text( String( format: "Y: %f", memberY )).font( .system( size: LIST_TXT_SIZE )) }
                Stepper(value: $memberZ, in: 0 ... 999999.9, step: 0.01 ){ Text( String( format: "Z: %f", memberZ )).font( .system( size: LIST_TXT_SIZE )) }
            }

            Spacer()

            Button( action:{

                Ps.PrmsAtoF.MemberA = memberA
                Ps.PrmsAtoF.MemberB = memberB
                Ps.PrmsAtoF.MemberC = memberC
                Ps.PrmsAtoF.MemberD = memberD
                Ps.PrmsAtoF.MemberE = memberE
                Ps.PrmsAtoF.MemberF = memberG

                Ps.PrmsUtoZ.MemberU = memberU
                Ps.PrmsUtoZ.MemberV = memberV
                Ps.PrmsUtoZ.MemberW = memberW
                Ps.PrmsUtoZ.MemberX = memberX
                Ps.PrmsUtoZ.MemberY = memberY
                Ps.PrmsUtoZ.MemberZ = memberU

                // 画面を閉じて呼び出し画面に戻る.
                my_dismiss_action()

            })
            {
                Text( "Change & Close" )
                    .font( .system( size: BTN_TXT_SIZE ))
                    .foregroundColor( .white )
            }
            .padding( BTN_TXT_PADDING )
            .background( .blue )

            Spacer()

        }
        .onAppear(){

            memberA = Ps.PrmsAtoF.MemberA
            memberB = Ps.PrmsAtoF.MemberB
            memberC = Ps.PrmsAtoF.MemberC
            memberD = Ps.PrmsAtoF.MemberD
            memberE = Ps.PrmsAtoF.MemberE
            memberG = Ps.PrmsAtoF.MemberF

            memberU = Ps.PrmsUtoZ.MemberU
            memberV = Ps.PrmsUtoZ.MemberV
            memberW = Ps.PrmsUtoZ.MemberW
            memberX = Ps.PrmsUtoZ.MemberX
            memberY = Ps.PrmsUtoZ.MemberY
            memberZ = Ps.PrmsUtoZ.MemberZ

        }

    }
}

struct View000_Previews: PreviewProvider {
    static var previews: some View {

        // aaaApp.swift と同様に、プレビューコードとしてパラメータを持つクラスを関連づける.
        View000()
            .environmentObject( ParametersSystemOperation() )

    }
}

グローバルに参照できるクラスを @EnvoronmentObject として扱う限りは、そのメンバは @State な動きができないようです。

なので仕方なくメンバ変数と同じような @State な変数を定義しました。何かいい方法がみつかったら、本サイトでご紹介する予定です。もっともっと調査してみます。