こんにちは。フエルマネー開発者です。
この度コードを1から書き直すという暴挙に出た次第ですが、前回に引き続いて次に行うことはズバリ、画面遷移設計ですね。
NavigationStackとは
iOS16時点で推奨されている、画面遷移のためのViewです。
以前のNavigationViewを使っていて、なかなか移行に腰が上がらない人もいるでしょう。僕も当初はNavigationViewの機能だけで済ませようと思っていました。
でもここでついていかないと、時代に取り残されて未だにObjective-Cで開発している人の二の舞になると思ったので移行しました。
意外と楽しいので、ここで使いこなせるようになっておきましょう。
何ができるの?
NavigationStackは、pathに格納した情報をもとに、画面遷移状態すなわち現在地を把握することができます。
struct ContentView: View {
@State var path = NavigationPath()
var body: some View{
NavigationStack(path: $path){
Text("hoge")
.navigationTitle("ホーム")
.navigationBarTitleDisplayMode(.inline)
}
}
}
とりあえず、土台はこんな感じです。
.navigationBarTitleDisplayMode(.inline)は僕の好みなので、なくてもいいです。何が違うかは実際に変えて試してみてください。Navigationタイトルの見た目が変わるだけです。
NavigationStackの引数pathにBindingした値が、画面遷移状態を把握するためのStateとなります。このpathの値は、条件を満たすコレクションならなんでも(Int配列、String配列などでも)良いらしいですが、
@State var path = NavigationPath()
NavigationPathというコレクションはこれのためだけ(?)に生まれたなんでも入る配列です。
結論、これを使えば良いという話です。
基本的な画面遷移
まずは以下をご覧ください。
struct ContentView: View {
@State var path = NavigationPath()
var body: some View{
NavigationStack(path: $path){
Button("遷移する"){
path.append(0)
}
.navigationTitle("ホーム")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: Int.self, destination: { _ in
Text("hoge")
})
}
}
}
結果
以下のモディファイアにて、pathにIntがappendされた時に遷移するDestination(=View)を定義します。
.navigationDestination(for: Int.self, destination: { _ in
Text("hoge")
})
Intであればなんでも反応します。つまりIntの中身によって遷移先の情報を変えられます。
struct ContentView: View {
@State var path = NavigationPath()
var body: some View{
NavigationStack(path: $path){
VStack{
Button("hoge0に遷移する"){
path.append(0)
}
Button("hoge1に遷移する"){
path.append(1)
}
Button("hoge2に遷移する"){
path.append(2)
}
Button("hoge3に遷移する"){
path.append(3)
}
Button("hoge4に遷移する"){
path.append(4)
}
}
.navigationTitle("ホーム")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: Int.self, destination: { appended in
Text("hoge\(appended)")
})
}
}
}
これはIntでなくても、Stringでも良いですし、オリジナルのクラスでも構いません。
ただし注意して欲しいのは、遷移先(子画面としましょう)で同様にIntをappendすると、親画面で定義した上記のNavigationDestinationが呼ばれます。複数定義するとエラーの元となるので好き勝手につけるのは危険です。
他のViewと絶対に干渉したくない方は、このView内に専用の列挙体を作ることをお勧めします。
struct ContentView: View {
@State var path = NavigationPath()
+ enum InnerPath: Int{
+ case hoge0, hoge1, hoge2, hoge3, hoge4
+ }
var body: some View{
NavigationStack(path: $path){
VStack{
Button("hoge0に遷移する"){
+ path.append(InnerPath.hoge0)
}
Button("hoge1に遷移する"){
+ path.append(InnerPath.hoge1)
}
Button("hoge2に遷移する"){
+ path.append(InnerPath.hoge2)
}
Button("hoge3に遷移する"){
+ path.append(InnerPath.hoge3)
}
Button("hoge4に遷移する"){
+ path.append(InnerPath.hoge4)
}
}
.navigationTitle("ホーム")
.navigationBarTitleDisplayMode(.inline)
+ .navigationDestination(for: InnerPath.self, destination: { appended in
+ Text("hoge\(appended.rawValue)")
+ })
}
}
}
NavigationLinkを用いた遷移
NavigationLinkは以前からありますが、馴染みのない引数「value」があるかと思います。これが先ほどpathにappendしてた値に対応します。
つまり先ほどの基本的な遷移では、コードベースでいつでもpathにappendすれば遷移できましたが、簡単な遷移であればNavigationLinkで事足ります。タップしたらvalueに設定した値がpathにappendされます。
struct ContentView: View {
@State var path = NavigationPath()
enum InnerPath: Int{
case hoge0, hoge1, hoge2, hoge3, hoge4
}
var body: some View{
NavigationStack(path: $path){
VStack{
NavigationLink("hoge0に遷移する", value: InnerPath.hoge0)
NavigationLink("hoge1に遷移する", value: InnerPath.hoge1)
NavigationLink("hoge2に遷移する", value: InnerPath.hoge2)
NavigationLink("hoge3に遷移する", value: InnerPath.hoge3)
NavigationLink("hoge4に遷移する", value: InnerPath.hoge4)
}
.navigationTitle("ホーム")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: InnerPath.self, destination: { appended in
Text("hoge\(appended.rawValue)")
})
}
}
}
一気にスッキリしましたね。
このようにNavigationStackを使うと、似たような内容の遷移を一挙に書けるのでコードがコンパクトになります。Destinationを一個ずつ書く必要はありません。
上級者編
さて、ここからが本題です。ここまでは基本的なNavigationStackの説明でしたが、ここからは応用編で、さらにコードを簡潔にするアイデアを共有します。
まずは以下をご覧ください。
enum HomePath: Int{
case root, setting, account, variable, fixed, premium, explain, information
var toString: String{
["ホーム", "設定", "資産を管理", "変動収支を管理", "固定収支を管理", "プレミアム会員になる", "操作説明", "お知らせ"][self.rawValue]
}
@ViewBuilder
func Destination() -> some View{
switch self {
case .root: HomeView()
case .setting: SettingView()
case .account: AccountView()
case .variable: VariableView()
case .fixed: FixedView()
case .premium: PremiumView()
case .explain: ExplainView()
case .information: InformationView()
}
}
}
これは、フエルマネーのホーム画面以下の画面遷移設計です。
ホーム画面(ContentView)からは設定画面(SettingView)、操作説明画面(ExplainView)、お知らせ画面(InformationView)の3つに遷移できます。
設定画面(SettingView)からは、資産管理(AccountView)、変動収支管理(VariableView)、固定収支管理(FixedView)、プレミアム登録画面(PremiumView)に遷移できます。
実際の画面遷移は階層構造になっているのですが、この列挙体ではそれをガン無視して全画面を列挙しています。
これの何が良いかというと、、、
struct ContentView: View {
@State var path = NavigationPath()
var body: some View{
NavigationStack(path: $path){
List{
NavigationLink(HomePath.setting.toString, value: HomePath.setting)
NavigationLink(HomePath.explain.toString, value: HomePath.explain)
NavigationLink(HomePath.information.toString, value: HomePath.information)
}
.navigationTitle(HomePath.root.toString)
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: HomePath.self, destination: { appended in
appended.Destination()
.navigationTitle(appended.toString)
.navigationBarTitleDisplayMode(.inline)
})
}
}
}
子階層含め全ての画面遷移がルートのホーム画面で定義できます。しかもNavigationTitleまで対応しています。
なぜかというと、遷移先の子画面で同じクラスのpath(ここでいうHomePath)を入れても、親画面の.navigationDestinationが呼ばれるからです。
SettingViewでもさらに孫階層へ画面遷移をするわけですが、その処理はHomeView上で完結しています。なのでSettingViewでやることといえば
struct SettingView: View{
var body: some View{
List{
NavigationLink(HomePath.account.toString, value: HomePath.account)
NavigationLink(HomePath.variable.toString, value: HomePath.variable)
NavigationLink(HomePath.fixed.toString, value: HomePath.fixed)
NavigationLink(HomePath.premium.toString, value: HomePath.premium)
}
}
}
たったのこれだけなんです。
他の画面も適当に(とりあえずTextだけで)作って、実行してみましょう。
これだけ豊富な遷移画面を作っているのに、なんとコード行数は100行を切ります。
皆さんもSwiftUIを使いこなしてスタイリッシュな開発を楽しんでください!
一応、全体のソースを貼っときます。
import SwiftUI
struct ContentView: View {
@State var path = NavigationPath()
var body: some View{
NavigationStack(path: $path){
List{
NavigationLink(HomePath.setting.toString, value: HomePath.setting)
NavigationLink(HomePath.explain.toString, value: HomePath.explain)
NavigationLink(HomePath.information.toString, value: HomePath.information)
}
.navigationTitle(HomePath.root.toString)
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: HomePath.self, destination: { appended in
appended.Destination()
.navigationTitle(appended.toString)
.navigationBarTitleDisplayMode(.inline)
})
}
}
}
struct SettingView: View{
var body: some View{
List{
NavigationLink(HomePath.account.toString, value: HomePath.account)
NavigationLink(HomePath.variable.toString, value: HomePath.variable)
NavigationLink(HomePath.fixed.toString, value: HomePath.fixed)
NavigationLink(HomePath.premium.toString, value: HomePath.premium)
}
}
}
enum HomePath: Int{
case root, setting, account, variable, fixed, premium, explain, information
var toString: String{
["ホーム", "設定", "資産を管理", "変動収支を管理", "固定収支を管理", "プレミアム会員になる", "操作説明", "お知らせ"][self.rawValue]
}
@ViewBuilder
func Destination() -> some View{
switch self {
case .root: ContentView()
case .setting: SettingView()
case .account: AccountView()
case .variable: VariableView()
case .fixed: FixedView()
case .premium: PremiumView()
case .explain: ExplainView()
case .information: InformationView()
}
}
}
struct AccountView: View{
var body: some View{
Text("AccountView")
}
}
struct VariableView: View{
var body: some View{
Text("VariableView")
}
}
struct FixedView: View{
var body: some View{
Text("FixedView")
}
}
struct PremiumView: View{
var body: some View{
Text("PremiumView")
}
}
struct ExplainView: View{
var body: some View{
Text("ExplainView")
}
}
struct InformationView: View{
var body: some View{
Text("InformationView")
}
}